您当前的位置: 首页 > 

合天网安实验室

暂无认证

  • 0浏览

    0关注

    748博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Codegate CTF和HackTM CTF的两个web题解

合天网安实验室 发布时间:2020-02-26 10:27:31 ,浏览量:0

前言

在家无聊,就打了两个ctf,总结一下:

0x01 renderer 0x001 题目描述如下:
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
0x002 首页如下:

首页只有一个url的提交框感觉应该是考SSRF。 我们随便访问一下:用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

复制上方链接或者点击阅读原文做实验。

0x003 获取源码 题目给我们提供了源码因此我们先下来看看。 /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的目录位置因此我们可以通过这个配置漏洞来遍历敏感文件。 0x004 代码下载与解析: 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等框架。 感兴趣的可以查看这篇文章https://uwsgi-docs-cn.readthedocs.io/zh_CN/latest/WSGIquickstart.html 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{}")
该flask框架是使用蓝图的模块化应用,并且我们可以看到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


1.首先代码有一处比较明显的漏洞在admin_ticket()中使用这个render_template_string()函数渲染字符串,这是一个ssti注入,相信大家不会陌生。 2.但是我们想要利用需要ip=ripip["127.0.0.1","127.0.0.2"]中并且User-Agent="AdminBrowser/1.337",还有ticket文件名必须知道。 3.经过了许久的苦思之后,后来同学丢给我一个链接,经他提醒才知道是CRLF注入。 下面我们就一起来梳理一下: 1.当我们访问/renderer/时会调用index()函数,利用ssrf和CRLF注入我们可以使ip等于127.0.0.1rip等于{{config.FLAG}},由于ip != rip那么将会把rip写入到/home/tickets/的某个文件中(文件名为数字),然后通过admin_remote.html文件将文件名ticket显示在其中:
{% extends "base.html" %}
{% block usertyle %}

{% endblock %}
{% block body %}
Codegate '20 Proxy Admin Page {% if ticket %}

Your access log is written with ticket no {{ ticket }}

{% endif %}
{% endblock %}
上面的admin_remote.html是用ssrf渲染的然后再将其作为数据渲染显示在index.html中这样我们就拿到了ticket的值: 下面是请求的过程:

2.根据上面的步骤我们已经将恶意代码写入到了/home/tickets/0008651ea04209ff2d014745533034d815ea9707文件当中,现在我们就要把他读取出来作为render_template_string(log)的参数渲染就可以拿到flag了。 3.跟上面一样我们访问/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。 下面是请求的过程:

成功获取flag:
CODEGATE2020{CrLfMakesLocalGreatAgain}
相关实验:flask服务端模板注入漏洞

http://www.hetianlab.com/cour.do?w=1&c=CCID9565-ac81-488a-b97e-c6d1b9cd978e

0x02 Draw with us 完整源码请在公众号回复关键词 源码 获取。 0x001 题目源码链接如下: stripped.js 获取flag是我们的目标,因此我们需要从怎么获取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,签名是安全的。 刚开始我尝试了JWTnone攻击,构造方法如下:
{
  "id": "dff3dc0b-b6fd-494e-8a8b-329fc600f4fb",
  "iat": 1581076667
}
改成:
{
  "id": "0",
  "iat": 1581076667
}


{
  "alg": "HS256",
  "typ": "JWT"
}
改成
{
  "alg": "none",
  "typ": "JWT"
}
但是没有用。 参考链接如下: https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/ 使用构造工具如下: https://jwt.io/ 0x002 我们继续阅读上面的源码,在/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 }) }));
从上面我们知道要获取flag我们需要让adminId0,因此需要target^pwHash0这意味着target===pwHash。 1.target是这个config.n的md5值。 2.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,因此我们需要去添加np到我们的用户权限列表中,但是只要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()。 从上面的代码中我们可以看到adminUsernamehacktm如果我们尝试去登陆(/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来绕过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            
关注
打赏
1665306545
查看更多评论
0.0545s