XMind: 蓝队视角下的防御体系突破.xmind
PDF: 蓝队视角下的防御体系突破.PDF
侵删。
]]>https://twitter.com/chybeta/status/1176702424045772800
中文: https://xz.aliyun.com/t/6419
https://seclists.org/fulldisclosure/2019/Sep/31
The first parameter routestring
tell what template should vBulletin look for.
In the callRender()
,$routeInfo[2]
will be set as widget_php
and $params
will contains the render config $widgetCongi[code]
In \core\install\vbulletin-style.xml
,we can fidn a template named widget_php
So when $widgetConfig['code']
is not null and the setting disable_php_rendering
isn’t disabled, vBulletin will use the following syntax to render template:
In includes\vb5\frontend\controller\bbcode.php
, you can find how evalCode
defined:
Finally cause PHP-Template injection and pre-auth RCE in vBulletin 5.x。
中文:https://xz.aliyun.com/t/6040
To reproduce this vulnerability, you need enable the password-change feature.
https://ip:10000/webmin/edit_session.cgi?xnavigation=1 :
Then you can check the config and the passwd_mode
value has been changed
You can capture post request like this:
Set the parameter old
value as |ifconfig
In password_change.cgi :
The code will check whether the parameter user
is a Webmin user. If there is a Webmin user named root
and we set user=root
,then the $wuser
‘s value will be root
.
If we set user=xxxx
,then $wuser
will still be undef
after grep
。
However the following is $wuser->{'pass'}
,which will change $wuser
value from undef
to {}
So whatever user
you have provided, you will be step in the code segment to update webmin user’s password.
Now let’s check the password_change.cgi
line 37 ~ line 40:
The implemention of function encrypt_password
is of no importance . You should pay attention to how Webmin handles the error message.
|
|
Webmin just put our parameter old
in qx/.../
!
And after executing system commands, Webmin will print the result:
So in conclusion there is no need to add a vertical bar (|)
, we just set our parameter old
value as ifconfig
By the way , there is an interesting issue https://github.com/webmin/webmin/issues/947
webmin 1.930 fix this security vulnerability by removing the qx()
backdoor:
https://pivotal.io/security/cve-2019-3799
DEMO: https://github.com/spring-cloud/spring-cloud-config#quick-start
|
|
Spring Cloud Config provides server and client-side support for externalized configuration in a distributed system. With the Config Server you have a central place to manage external properties for applications across all environments.
According to the DOC,The Config Server provides these through an additional endpoint at /{name}/{profile}/{label}/{path}
where name
, profile
and label
have the same meaning as the regular environment endpoint, but path
is a file name (e.g. log.xml)。
For example if we want get test.json
as plain text, you can send this request:
So how the backend handle this request? When we send the payload, server
will dispatcher the request to org/springframework/cloud/config/server/resource/ResourceController.java:54
:
Step into retrieve
function which located inorg/springframework/cloud/config/server/resource/ResourceController.java:104
:
Continue step into the findOne
function:
You can see the locations
value is file:/tmp/config-repo-7168113927339570935/
. The Config-Server
will pull the remote repo and use the locations
folder to store these temporary files:
Notice the path
value is ..%2F..%2F..%2F..%2Fetc%2fpasswd
,so actually the full path like this :
at the end, when call StreamUtils.copyToString(is, Charset.forName("UTF-8")
, we can read the /etc/passwd
content:
https://github.com/spring-cloud/spring-cloud-config/commit/3632fc6f64e567286c42c5a2f1b8142bfde505c2
The backend will check whether the resource paths is valid via isInvalidPath
and isInvalidEncodedPath
:
:
https://confluence.atlassian.com/doc/confluence-security-advisory-2019-03-20-966660264.html
According to the document , there are three parameters that you can set to control the content or format of the macro output, including URL、Width and Height.
the Widget Connector
has defind some renders. for example the FriendFeedRenderer
:
In FriendFeedRenderer
‘s getEmbeddedHtml
function , you will see they put another option _template
into params map.
However, some other renderers, such as in video
category , just call render(getEmbedUrl(url), params)
directly
So in this situation, we can "offer"
the _template
ourseleves which the backend will use the params to render
|
|
in fix version, it will call doSanitizeParameters
before render html which will remove the _template
in parameters. The code may like this:
https://groups.google.com/forum/#!topic/rubyonrails-security/pFRKI96Sm8Q
The render method can use a view that’s entirely outside of your application. So in actionview-5.2.1/lib/action_view/renderer/template_renderer.rb:22
, it will call find_file
to determine which template to be rendered。
In the find_file
method:
step into args_for_lookup
method which to generate the options. When it returns, our payload will be saved in details[formats]
:
then it will execute @view_paths.find_file
which located in actionview-5.2.1/lib/action_view/path_set.rb
:
Because the view is outside of your application,so outside_app
equalsTrue
and then will call find_all_anywhere
Skip cached
part, the find_templates
will according the options to find the template to render:
|
|
After build_query
, the variables :
SO here we use ../
to make directory traversal,and use double {
to make sure syntax right. After File.expand_path
, the result is:
|
|
so the /etc/passwd
will be treated the template to be rended ,which lead to a arbitrary file read attack.
install vulnerable Rails (e.g 5.2.1)
Generate controller:
Inapp/controllers/chybeta_controller.rb
:
add resources in config/routes.rb
:
https://github.com/rails/rails/commit/f4c70c2222180b8d9d924f00af0c7fd632e26715
]]>Affected Versions: Nexus Repository Manager 3.6.2 OSS/Pro versions up to and including 3.14.0
Fixed in Version: Nexus Repository Manager OSS/Pro version 3.15.0
Nice find from Rico @ Tencent Security Yunding Lab and voidfyoo @ Chaitin Tech
In plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/ComponentComponent.groovy:185
Nexus introduced CSEL based selectors to support changes coming in future releases. CSEL is a light version of JEXL used to script queries along specific paths and coordinates available to your repository manager formats. Step in browseService.previewAssets
,and its implementations in components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/internal/BrowseServiceImpl.java:233
Pay attention to the comment: whereClause
will run after repository filtering! We need to know how it is constructed. In the components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/internal/PreviewAssetsSqlBuilder.java:51
, which introduce contentExpression
and jexlExpression
:
So after repository filtering,whereClause
will run automatically which call contentExpression.execute()
method 。In components/nexus-repository/src/main/java/org/sonatype/nexus/repository/selector/internal/ContentExpressionFunction.java
According to the code contentExpression(@this, :jexlExpression, :repositorySelector, " +":repoToContainedGroupMap) == true
, you can map contentExpression parameters to iParams[i]
:
@this
-> iParams[0]
jexlExpression
-> iParams[1]
repositorySelector
-> iParams[2]
In last, it will call checkJexlExpression()
method:
|
|
So, we can step in selectorManager.evaluate
,which is implemented in components/nexus-core/src/main/java/org/sonatype/nexus/internal/selector/SelectorManagerImpl.java:156
,and finally evaluate the expression:
@Override
@Guarded(by = STARTED)
public boolean evaluate(final SelectorConfiguration selectorConfiguration, final VariableSource variableSource)
throws SelectorEvaluationException
{
Selector selector = createSelector(selectorConfiguration);
try {
return selector.evaluate(variableSource);
}
catch (Exception e) {
throw new SelectorEvaluationException("Selector '" + selectorConfiguration.getName() + "' evaluation in error",
e);
}
}
According to DOCS:
https://help.sonatype.com/repomanager3/configuration/repository-management#RepositoryManagement-CreatingaQuery
To reproduce the issue successfully, we need upload some assets to the repo firstly。For excample, upload a jar:
Then go here to intercept the request:
POC:
Add the permission requirement: @RequiresPermissions('nexus:selectors:*')
以 thinkphp 5.0.22 完整版为例,下载地址:http://www.thinkphp.cn/down/1260.html
未开启调试模式。
|
|
先整体的看一下这个流程,tp程序从 App.php
文件开始,其中截取部分如下:
在App.php
中,会根据请求的URL调用routeCheck
进行调度解析获得到$dispatch
,之后将进入exec($dispatch, $config)
根据$dispatch
类型的不同来进行处理。
在payload中,访问的url为index.php?s=captcha
。在vendor/topthink/think-captcha/src/helper.php
中captcha注册了路由,
因此其对应的dispatch
为method
:
一步步跟入,其调用栈如下:
通过调用Request
类中的method
方法来获取当前的http请求类型,这里顺便贴一下该方法被调用之处:
该函数的实现在 thinkphp/library/think/Request.php:512
在tp的默认中配置中设置了表单请求类型伪装变量如下
因此通过POST一个_method
参数,即可进入判断,并执行$this->{$this->method}($_POST)
语句。因此通过指定_method
即可完成对该类的任意方法的调用,其传入对应的参数即对应的$_POST
数组
Request
类的构造函数__construct
代码如下
利用foreach循环,和POST传入数组即可对Request
对象的成员属性进行覆盖。其中$this->filter
保存着全局过滤规则。经过覆盖,相关变量变为:
注意我们请求的路由是?s=captcha
,它对应的注册规则为\think\Route::get
。在method
方法结束后,返回的$this->method
值应为get
这样才能不出错,所以payload中有个method=get
。在进行完路由检测后,执行self::exec($dispatch, $config)
,在thinkphp/library/think/App.php:445,由于$dispatch
值为method
,将会进入如下分支:
跟入Request::instance()->param()
,该方法用于处理请求中的各种参数。
如上方法中$this->param
通过array_merge
将当前请求参数和URL地址中的参数合并。回忆一下前面已经通过__construct
设置了$this->get
为dir
。此后$this->param
其值被设置为:
继续跟入$this->input
:
该方法用于对请求中的数据即接收到的参数进行过滤,而过滤器通过$this->getFilter
获得:
前面$this->filter
已经被设置为system
,所以getFilter
返回后$filter
值为:
回到input
函数,由于$data
是前面传入的$this->param
即数组,所以接着会调用array_walk_recursive($data, [$this, 'filterValue'], $filter)
,对$data
中的每一个值调用filterValue
函数,最终调用了call_user_func
执行代码:
回想前面的调用链,param -> method -> input -> getFilter -> rce。因为filter
可控,而tp的逻辑会对输入即input进行filter
过滤,所以重点是找到一个合理的input
入口。
回到param
方法:
跟入$this->method(true)
注意此时的参数为true
,所以此处会进入第一个分支:
继续跟入$this->server
,可以发现这里也有一个input
!
所以对input
方法而言,其$data
即$this->server
数组,其参数name
值为REQUEST_METHOD
,在input
方法源码如下:
因此利用前面的__construct
,可以通过传入server[REQUEST_METHOD]=dir
,使得在经过foreach
循环时置$data
值为dir
,此后调用getFilter
,同样实现RCE:
给出payload:
补丁地址:https://github.com/top-think/framework/commit/4a4b5e64fa4c46f851b4004005bff5f3196de003
问题的根源在于请求方法的获取接收了不可信数据,因此补丁中设置了白名单,如下
这里仅仅测试了5.0.22 完整版本。各个版本之间代码有些许差异,payload不一定通用,建议自己调试调试。
]]>项目地址:https://github.com/m4ll0k/WAScan.git
类型 | 名 | 作用 |
---|---|---|
dir | lib | 扩展,攻击用到的一些字典等等 |
dir | plugin | 主要攻击脚本 |
dir | screen | 一些截图 |
file | .gitignore | 略 |
file | LICENSE | 许可证 |
file | README.md | 介绍 |
file | wascan.py | 主入口文件 |
所有文件
主入口文件。会先初始化一些Usage
,接受命令行参数并进行相关的前期处理。然后根据参数开始进行扫描。
|
|
定义了一个wascan
类,通过getopt.getopt
接受命令行参数。对应代码如下:
|
|
scan
参数为扫描类型,对应如下:
scan值 | 扫描类型 |
---|---|
0 | 指纹Fingerprint |
1 | 攻击Attacks |
2 | 审计Audit |
3 | 爆破Brute |
4 | 信息搜集Disclosure |
5 | 全面扫描 |
对应代码如下:
主要定义一些匹配模式,用于查找页面上的各种信息。
获取信用卡信息
获取ip
获取邮箱
|
|
parse
类,进行真正的信息搜集工作。定义了clean
方法,将响应中的各种标签,各种可能的符号直接replace
掉,然后再进行真正的搜索。简单粗暴。
主要是定义一些跟请求
相关的方法/类/功能
如名,爬虫。爬取页面上的所有连接。
接下来是爬虫类SCrawler
,它继承自Request
类。
生成随机的 User-Agent。命令行选项wascan.py --ragent
开启。
基本请求。包括请求/代理认证,请求,重定向,响应的处理。
两个方法用于请求/代理认证
Request类,发送基本请求,处理头部参数,认证、代理、cookie、超时等问题。
NoRedirectHandler
,不进行跳转。
响应处理类。获取响应内容,响应url,响应的status_code,响应的头部。
主要是定义一些小功能、小工具
无,跳过
如名,主要进行一些前期的检查准备。
定义一些颜色常量,略过。
定义了dirs
函数,用于列举出指定目录下,指定后缀名为py
,且不是__init__.py
的 py文件。
测试用例如下:
定义了几种可能出现的错误:
定义了两个类,用于处理请求参数
和payload
的关系,替换和拼接。替换的场景,比如任意文件读取,?readfile=xx
可能替换成?readfile=/etc/passwd
。拼接的场景,比如SQL注入,?id=1
,可能拼接为 ?id=1'
或者 ?id=1" or 1=1
第一个类preplace
替换,用于把请求参数
的值替换为对应的payload
。存疑一:get请求中用sub(porignal,ppayload,self.url)
来处理,而post请求中用self.data.replace(porignal,ppayload
请求。
|
|
第二个类padd
,用于往请求参数中添加payload。
整合了基本攻击的各种payload。对于每种攻击,返回list。结合前面整体功能 -> 攻击
章节:
类型 | 对应函数payload |
---|---|
Bash 命令注入 | bash() |
SQL盲注 | bsql() |
溢出 | None |
CRLF | crlfp() |
头部SQL注入 | None |
头部XSS | None |
HTML注入 | html() |
LDAP注入 | ldap() |
本地文件包含 | plfi() |
执行操作系统命令 | os() |
php 代码注入 | php() |
SQL注入 | sql() |
服务器端注入 | ssip() , pssi() |
Xpath注入 | xpath() |
XSS | pxss() |
XML注入 | xxep() |
头部SQL注入
、溢出
、头部XSS
在该文件中对应的payload似乎没有出现。payload的具体内容就这里不展开,具体等后文与调用代码结合解释。
|
|
定义了各种打印输出方法,基本的格式化字符串、颜色、编码等等。
定义两个函数。第一个是r_time
基于当前时间strftime('%y%m%d')
用来生成随机数字。
第二个是r_string
,用于生成指定长度为n
的包含大写或者小写字母的随机字符串。
该文件定义了readfile
函数,用于基本的文件读取操作。首先判断路径是否为空,!=None
或者!=""
。利用列表生成器,line.strip()
在读取每一行后去除两边的空白符。:
|
|
统一转换成utf-8
来处理
用来输出一些帮助信息,全程一行行print
,简单粗暴。
这里定义了几种扫描处理模式。回到主文件wascan.py
中,它真正开始扫描是后半部分代码,根据kwargs['brute']
或scan
的值去选择不同的模式,比如若指定了brute
,则会调用BruteParams
模式,其余类似。这些模式都整合在handler
目录下。
第一种暴破指去爆破页面中的隐藏参数
。brute.py
对应代码如下:
其中params
类后文再详解。
主文件wascan.py
的调用入口:
第二种爆破指后台爆破、路径爆破。brute.py
对应代码如下:
主文件wascan.py
中两处入口:
指纹识别模式。fingerprint.py
代码中Fingerprint
类如下:
在探测server
时,由于WAScan
直接采用了返回头部中的server
字段,没有爆破处理。所以server
函数实际存放在plugins/fingerprint/server/server.py
。而其他类型的指纹,比如cms
、framework
、Language
、Os
、Waf
等,难以直接确定,需要多种脚本去尝试,所以这几种类型的指纹探测,都是在fingerprint.py
中定义了一个入口函数,用来导入`plugins/fingerprint/
目录下的相关探测模块。
|
|
在完成所有类型的探测后,wascan
在结尾调用了Headers(req.headers,req.content)
,这个根据响应来确定一些信息,具体作用等讲解plugins/fingerprint
时再详说。
在主文件wascan.py
中有两处入口,如下:
导入各种攻击的模块,然后调用运行
主文件wascan.py
中的入口:
载入各种审计的模块,然后调用运行。
主文件wascan.py
中的入口:
载入各种信息搜集的模块,然后调用运行。
主文件wascan.py
中的入口:
爬虫调用,在给定一个url后,在fullscan模式下会去爬去页面中所有的链接,然后进行检查。对应代码如下:
links
保存所有的url,一开始就一个。然后通过调用爬虫:lib/request/crawler.py
中的SCrawler
爬虫,不断地往links
中添加,然后不断爬取。
主文件的入口:
实际代码如下:
主文件入口:
所以综上,fullscan
模式的整体流程如下:
Fingerprint()
Crawler()
FullScan()
Attacks()
Disclosure()
Audit()
Brute()
整合各种字典。先略过。
检查HTML代码注入。思路即:在参数值中添加进html代码,然后检查返回的响应,直接用search(payload,req.content)
来看能否检测到相应的模式,。若存在则保存URL
、DATA
、PAYLOAD
,然后输出。
|
|
检查PHP代码注入。采用的是 system("cat /etc/passwd")
类似的payload来检测在返回的响应中匹配的是 root: /bin/bash
字符串,或者通过system("echo")
输出随机字符串来匹配。个人看法,system
在许多情况下都是被禁用的,因此通过system
来检测成功率估计不高。另外/etc/passwd
只存在UNIX系统上,win需要其他方式来检查。如果用phpinfo()
可能会更好。
|
|
对应的payload 在 lib/utils/payload.py:68 :
因为这个情况往往存在UNIX系统中,win一般不存在该漏洞。所以payload中只尝试读取/etc/passwd
,然后检测响应。
|
|
对应payload:
溢出bufferoverflow的payload没有在lib/utils/payload.py
中出现,而是直接定义在了这里。几种可能的字符,然后三种可能的长度,发包检测响应。这里的serror
需要匹配的模式(lib/db/errors/buffer.json)如下:
bufferoverflow.py
代码结构和 bufferoverflow.py 大致相同。
真正的payload 在 lib/utils/payload.py:137:
用于匹配的模式 lib/db/errors/lfi.json:
代码结构与 htmli.py 类似。
对应payload 在 lib/utils/payload.py:51:
代码结构与 bufferoverflow.py 类似。
payload 在 lib/utils/payload.py:75:
用于匹配的模式在 lib/db/errors/xpath.json:
payload中注入的模式是Set-Cookie:crlf=injection
,在进行检测时把=injection
替换成随机字符串。然后在返回头的Set-Cookie
(若有)中检测注入的随机字符串。
|
|
对应payload 在 lib/utils/payload.py:21:
代码结构与 htmli.py 类似。根据payload,直接在响应中去匹配特殊字符if search('{}'.format(payload.split('"')[1]),req.content):
。
对应payload在 lib/utils/payload.py:124
代码结构与 bufferoverflow.py 类似。
payload 在 lib/utils/payload.py:197:
用于匹配的模式在 lib/db/errors/xpath.json:
检查存在于头部字段的XSS,包括cookie
字段,referer
字段,useragent
字段。其实就是拿xss的payload放在对应的位置再打一圈。话说这个位置的xss危害不大吧。。
代码结构与 bufferoverflow.py 类似。
payload 在 lib/utils/payload.py:101:
用于匹配的模式在 lib/db/sqldberror/ 下。略过不提。
代码结构与 htmli.py 类似。发送请求,然后匹配if search(payload,req.content):
。个人看法,匹配效果较差。
payload在 lib/utils/payload.py:33:
bash注入,但是这里只检测了GET方法,POST请求并不检查!另外这里在 头部的User-Agent
、Referer
字段插入了payload。
|
|
payload定义在:
先休息一下。。
tpye.php中:
先看一下require
进来的include/common.inc.php
,在这个文件第58行中存在如下代码:
上面这段代码会通过@extract()
将尚未注册的变量进行注册,如果有冲突,不覆盖已有的变量。因此通过这个伪全局可以绕过if(empty($template)) $template = 'type';
这句话的指定,即$template
变量可控。
跟入template
函数,定义在 include/global.func.php:772
这里会进行一些判断,TPL_REFRESH
表示是否开启模板缓存自动刷新,默认为1, 剩下的用于判断缓存超时。倘若需要更新缓存则进入了template_compile()
函数,根据上一句的require_once
可知定义在 include/template.func.php:2
|
|
重点看$content = ($istag || substr($template, 0, 4) == 'tag_')
这一句。由于$template
可控,只要$template
以tag_
开头,就可以使得此处的三元表达式进入到第一个分支中,即相当于:
由于$template
未经过滤,被直接拼接到内容中,所以如果指定tag_(){};@unlink(_FILE_);assert($_GET[1]);{//../rss
,则拼接后的结果为
可以看到一句话木马已经写入了$content
,之后file_put_contents($compiledtplfile, $content);
将内容写入文件。
回到前面的template_compile
函数中,TPL_CACHEPATH
为常量PHPCMS_ROOT.'data/cache_template/
; 可知 $compiledtplfile
为:
所以payload末尾的../
利用目录穿越使得最后的$compiledtplfile
为'data/cache_template/rss.tpl.php
为了解析不出错,payload末尾处的//
注释了拼接后的其余部分,如上图。
此后访问 http://127.0.0.1/phpcms/data/cache_template/rss.tpl.php?1=phpinfo()
]]>source/module/misc/misc_ranklist.php:166
Dz在此处获取到$member['note']
后调用了dhtmlspecialchars
进行过滤,在source/function/function_core.php:203 会对’&’, ‘“‘, ‘<’, ‘>’进行实体编码。
从getranklist_members
返回后 source/include/misc/misc_ranklist_index.php:113
进行模板的渲染在 data/template/1_diy_ranklist_ranklist.tpl.php:32
可以看到在tip
属性中输出了$memberlist['0']['note']
。在之前有一个onmouseover
事件,跟入showTip(trhis)
在 static/js/common.js:1062
跟入_showTip
,在 static/js/common_extra.js:912
通过ctrlobj.getAttribute('tip')
获取tip属性的值,由于getAttribute
获取的内容会自动反转义,即前面在dhtmlspecialchars
编码过的内容又被解码了一次。此后拼接到div标签的innerHTML
中,最后输出到页面上造成了xss
关于getAttribute
,可以用下面代码测试:
该CMS中,排行榜功能是默认开启的。在地址 http://127.0.0.1/misc.php?mod=ranklist&type=member 的上榜宣言中输入payload(拒绝伸手党)
在 http://127.0.0.1/misc.php?mod=ranklist 当鼠标移动到头像上触发onmouseover
事件,执行xss
多增加一次dhtmlspecialchars
。
|
|
从 https://github.com/requests/requests/releases?after=v0.3.0 知道 v0.2.0 发布时的 commit为 https://github.com/requests/requests/commit/d2427ecae751a533ddd9026849dd19cfaa3394f4 。检出。
name | usage |
---|---|
docs | 保存文档 |
requests | 保存源代码 |
.gitignore | 略 |
HISTORY.rst | 历史 |
LICENSE | 协议 |
README.rst | readme |
setup.py | 安装 |
test_requests.py | 测试 |
定义如上方法,用于进行功能测试。
主要关注 core.py
UML图:
Structure:
主要实现四种类:请求基类_Request
、请求类Request
、响应类Response
、认证AuthObject
,七种方法:get、post、put、delete和认证相关的方法,四种异常类。
对urllib2.Request
对象 的封装,允许对请求方法进行s手动设置。
附上一些私有变量和私有方法:
Request类主要用于发送请求,因此重点关注其中的send
方法,注释中解释了几点:
True
,失败返回False
self.response.status_code
会包含错误代码sent
属性会变为True
anyway
参数若被设为True,则请求一定会被发送,不管是否曾发送过,
|
|
在send中,会先进行self._checks()
检查:
这里只检测了URL是否设置,若没有则抛出URLRequired
错误。然后根据method
的不同分情况send请求,如果发送成功则success为True,sent变量也为True,然后返回success变量。
添加注释,代码如下:
|
|
添加注释,代码如下:
添加注释,代码如下:
在 Request
类中我们见到在Request初始化__init__
时设置了self.response = Response()
。然后根据请求方法的不同,设置状态码self.response.status_code
、响应头部self.response.headers
、响应内容self.response.content
。接下来就看看response
类是如何实现的。
|
|
该类暂时仅在 test_requests.py
中出现,用于设置认证的用户名和密码。代码如下:
get、post、put、delete和认证相关的方法 ,在代码结构上大同小异。
|
|
|
|
|
|
|
|
|
|
从上面的请求方法实现中,可以发现有的请求带了如r.auth = _detect_auth(url, auth)
对于种种请求方法,我们不想在每次请求中都明确指出这次请求需不需要认证,但有些请求确实需要认证,因此在各种请求方法中都有一个可选参数auth=None
,然后通过调用r.auth = _detect_auth(url, auth)
来进一步设置。_detect_auth
代码如下
|
|
对于明确指出需要认证的请求,自然auth
参数也会指定。如果auth
参数没有指定,则会调用_get_autoauth
来查看是否有对应的规则。这个规则列表则由全局变量AUTOAUTHS
来维护,如果请求的url包含autoauth_url
,则返回autoauth_url
对应的auth。如果不包含,则直接返回None
。
为了维护这个全局变量AUTOAUTHS
,它实现了一个add_autoauth
方法如下:
|
|
不做过多解释。
https://github.com/heroku-python/pip-pop
按照commit记录来阅读。
commit记录: a84bc7439770063e457760a18119c10e5d802d3e
添加了LICENSE
文件,采用MIT License
commit记录: 636935f9394165c1d55c0e0d878cea60428a434e
创建了 pip_pop
文件夹,在其中创建空文件__init__.py
。 此时项目结构如下:
commit记录: ebdda7f8897403e9b77a2fa7023b2f4f8df1ecaa
项目结构如下:
增加了README.rst
文件。用于说明该项目的用处,计划中实现的功能,未来可能实现的功能。
commit记录: f0e51cc56f55c4615e29b7a12264b20dbe12db66
项目结构如下:
增加了requirements.txt
文件。
commit记录: bf54913eaa70f9f505c414a7be328ff15040f37f
项目结构如下:
修改READEME.rst
文件。
commit记录: 2b444bc846071148dedf6773555e8b33f895765c
项目结构如下:
修改README.rst
文件
commit记录: fd65e4d148939f1c7405370e1f342f1fa1b3ea14
项目结构如下:
新增bin/pip-diff
,bin/pip-flatten
和setup.py
。
bin/pip-diff
和bin/pip-flatten
均是空文件。
setup.py
用于python库打包。代码如下:
从setuptools
导入setup
函数,其中参数的含义如下:
参数 | 含义 | 值 |
---|---|---|
name | 包名字 | pip-pop |
version | 包版本 | 0.0.0 |
url | 程序官网地址 | https://github.com/kennethreitz/pip-pop |
license | 授权信息 | MIT |
author | 程序作者 | Kenneth Reitz |
author_email | 作者邮箱 | me@kennethreitz.org |
description | 程序简单描述 | __doc__.strip(‘\n’) |
scripts | 指定可执行脚本,安装时脚本会被添加到系统PATH中 | [‘bin/pip-diff’, ‘bin/pip-flatten’] |
zip_safe | 不压缩包,以目录形式安装 | False |
platforms | 程序适合的平台 | ‘any’ |
install_requires | 安装时需要安装的依赖包 | [‘docopt’] |
classifiers | 分类信息 | 详细见下 |
commit记录: d58196205cea3a4650d68443dd90132bbd4b2b4e
项目结构如下:
更改了bin/pip-diff
文件。代码整体的格式如下:
第一行#!/usr/bin/env python
,用于为脚本语言指定解释器,这样可以直接./*.py
的方式执行,不要使用#!/usr/bin/python
,因为python可能不是安装在默认的环境。
第二行# -*- coding: utf-8 -*-
用于指定编码为 utf-8
,这样可以在py文件中写中文,方便写注释和消息。
最下面的if __name__ == '__main__':
的意思是,当该py文件被直接运行时,if __name__ == '__main__':
之下的main()
将被调用执行,当该py文件被以模块的形式导入时,if __name__ == '__main__':
不被运行。
main()
函数源代码如下:
通过args = docopt(__doc__, version='pip-diff')
来获取对应的命令行参数,参数要求见程序开头的那一段注释:
args
解析完命令行参数后,会返回一个Dict
类型。然后通过kwargs
解析出对应的变量。。--fresh
和--stale
的作用是Generates a diff between two given requirements files. Lists either stale or fresh packages.
。以命令行参数--fresh D:\temp\req1 D:\temp\req2
为例
然后程序进入diff(**kwargs)
, diff函数:
Requirements
对象定义如下,其中的diff
函数先暂时省略:
以Requirements(r1)
为例,传入的参数为D:\\temp\\req1
,在__init__
中进入self.load(reqfile)
,首先判断了文件的存在。然后对于文件中的每一行(for line in f:
),去除它末尾的换行符(line = line.strip()
),然后判断其是否以注释或控制字符开头([line.startswith(p) for p in IGNORABLE_LINES]
),若不是则将其加入到data
中。之后调用parse_requirements(data)
进行解析:
在pass之后,返回给r1
在r2
对象实例化后,进行results = r1.diff(r2)
,在class Requirements(object)
中定义了diff方法代码如下:
commit记录: d6ae563831228dd6d7e712d69763663032410391
项目结构如下:
根据参数的不同fresh
或者stale
,输出对应的结果。
req1内容如下:
req2内容如下:
则运行结果如下:
commit记录: 2c2ffe318e5c539fc3bdef4feda97c56c162062a
项目结构及代码部分未做改变。
删除了原 pip-diff
中的一些注释
commit记录: 58f9ae5f9668a7613f7c0f9f1c43a105b2604891
将VERSION_OPERATORS
从list
改为tuple
。 其余无变化。
commit记录: d1ff1029ca3d4bd765abe2d4e92b1c2700586702
项目结构变为:
删除了pip-pop/__init__.py
空文件
commit记录: d638b182d9302fa541efa48fbf99fa05f42a4565
项目结构未变
利用pip.req来解析req文件
commit记录:69d9e22c10734d463bde67c04cc469f0b0bce072
项目结构未变
因为直接利用pip.req来解析req文件,删除无用变量
commit记录: 0837d1133ee25c645d763f670f6683a20bf30240
只有当requirement.req
为真时,才添加到self.requirements
中。
附上最新版的pip中的 parse_requirements
的代码:
最后会返回一个迭代器
commit记录: 3862c2f9a2f72bb962e7ed15416109ee0ec3e5ae
项目结构变为:
setup.py
中:
pip-flatten
变为pip-grep
,代码如下:
commit记录:2116d8a7698bf8fece0ad5c32db9ec9f69c97e69
更新readme文档,添加pip-grep的使用说明
commit记录:2116d8a7698bf8fece0ad5c32db9ec9f69c97e69
commit记录: 78e3c31b3584bfb263c061317ccc798cfaddf061
增加silent参数选项。作用位置
commit记录: 94c553879358aff40da2c3d2f536acb184703166
添加silent
模式对not found
情况的支持
commit纪录:70af45d95fd38e0a93abdbdb400283dcc495a00f
修改了pip-grep
和pip-diff
,将其中的print 'xx'
改为print('xx')
commit记录:2aa545fb3b80d78670d923be4333e85f0abb7309
|
|
新增加一个finder=finder
参数,避免parse_requirements
失败。
commit记录:2dc013300c4b0fb605fa9dd2a3fba5ecc81ac20c
修改setup.py
,修改版本号为version='0.1.0'
commit记录: a3f9a4ba40c02d6bc26318e589ae2db11304203f
修改pip-grep
文件。
首先是Usage部分:
-p
,在grep找到的情况下,打印出requirement
commit记录: 27f35700c7d8affb1fc3b399bd77fe38fb82bba1
修改pip-diff
。
由于parse_requirements
中:
所以添加session
参数:
commit记录:90eba89335af5aa1285d179aa9ea6aa9725bd712
修改内容同上,增加session
参数。
commit记录:d572c00cc65a47f8d6e3d9446f8c21fb7aac685f
无
commit记录:097c4a94848897e693bf269150a49129d4019390
修改pip-diff
和pip-grep
的一些细节,增删参数。
commit记录:047dd63d5dd0a754d3e515bef7aa33d1246a548b
修改pip-diff
文件,增加excludes
参数选项,用于指定排除,不进行比较的packages包
|
|
commit记录:81587647408ff5adc13cc30a50ff84e36116505d
无他,修改README中的拼写错误
commit记录:4f5ebcd253ec299baf0f4cb10c99d06bc52cc91f
修改两个文件pip-diff
和pip-grep
pip-diff
中将project_name
改为name
。原因是pip版本升级,经过parse_requirements
后会是name
属性。但在8.1.2版本之前并不存在,因此需要在load时进行检测,增加代码如下:
commit记录:4dc238c79ca19974eeb434ec4be4285d7747bb38
修改setup.py中的版本号
commit记录:07562561ce6aa9c733a18135cf510fadd794433a
修改setup.py中的一些参数Programming Language
、Development Status
等
commit记录:99d9f36ad765535946af1fa9fc181d33668ee146
修改setup.py中的install_requires
,要求pip版本大于1.5.0
commit记录:47ad229596ade5024d9c4c4190e73972176bc58b
删除requirements.txt
中的无用条目
commit记录:433e02ec7e294e171557514c55412cc3e06c1e53
项目结构:
修改READEME.rst
、setup.py
、requirments.txt
,主要是增加了tox
的依赖,相关环境的安装。
新增文件tests
文件夹及其文件、.gitignore
、tox.ini
。
commit记录:a40d8850701f08c99d66cab2eedf283a0b326731
新增.travis.yml
。修改README.rst
文件
commit记录:e865cb31f4b43edd5f07aa8d40680d0b1eb08f28
阅读完毕。
]]>2018年9月21日,Destoon官方发布安全更新,修复了由用户“索马里的海贼”反馈的一个漏洞。
根据更新消息可知漏洞发生在头像上传处。Destoon中处理头像上传的是 module/member/avatar.inc.php 文件。在会员中心处上传头像时抓包,部分内容如下:
对应着avatar.inc.php代码如下:
这里通过$_FILES['file']
依次获取了上传文件扩展名$ext
、保存临时文件名$name
、保存临时文件完整路径$file
变量。之后通过new upload();
创立一个upload对象,等到$upload->save()
时再将文件真正写入。
upload
对象构造函数如下,include/upload.class.php:25:
这里通过foreach($_file as $file)
来遍历初始化各项参数。而savepath
、savename
则是通过__construct($_file, $savepath, $savename = '', $fileformat = '')
直接传入参数指定。
因此考虑上传了两个文件,第一个文件名是1.php
,第二个文件是1.jpg
,只要构造合理的表单上传(参考:https://www.cnblogs.com/DeanChopper/p/4673577.html),则在avatar.inc.php中
而在upload
类中,由于多个文件上传,$this->file
、$this->file_name
、$this->file_type
将foreach在第二次循环中被置为jpg文件。测试如下:
回到avatar.inc.php
,当进行文件保存时调用$upload->save()
,include/upload.class.php:50:
先经过几个基本参数的检查,然后调用$this->is_allow()
来进行安全检查 include/upload.class.php:72:
可以看到这里仅仅对$this->ext
进行了检查,如前此时$this->ext
为jpg
,检查通过。
接着会进行真正的保存。通过$this->set_savepath($this->savepath); $this->set_savename($this->savename);
设置了$this->saveto
,然后通过move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)
将file
保存到$this->saveto
,注意此时的savepath
、savename
、saveto
均以php为后缀,而$this->file
实际指的是第二个jpg文件。
综上,上传两个文件,其中第一个文件以php为结尾如1.php
,用于设置后缀名为php
;第二个文件为1.jpg
,jpg用于绕过检测,其内容为php一句话木马(图片马)。
然后访问http://127.0.0.1/file/temp/avatar1.php 即可。其中1
是自己的_userid
不过实际利用上会有一定的限制。
第一点是destoon使用了伪静态规则,限制了file目录下php文件的执行。
第二点是avatar.inc.php中在$upload->save()
后,会再次对文件进行检查,然后重命名为xx.jpg
:
因此要利用成功就需要条件竞争了。
在upload的一开始,就进行一次后缀名的检查。其中is_image如下:
在__construct()
的foreach中使用了break,获取了第一个文件后就跳出循环。
在is_allow()
中增加对$this->savename
的二次检查。
嘛,祝各位大师傅中秋快乐!
]]>2018年7月17日,Gitlab官方发布安全更新版本,修复了一个远程命令执行漏洞,CVE ID为CVE-2018-14364,该漏洞由长亭研究人员发现,并在hackerone平台提交
影响版本:>= 8.9.0
修复版本:11.0.4, 10.8.6, and 10.7.7
以版本11.0.3为例。根据版本源码对比
从CHANGELOG.md中得知为Fix symlink vulnerability in project import
主要修改的代码文件为lib/gitlab/import_export/file_importer.rb
主要关注一下extracted_files
。
当我们import一个项目时,会进入到file_import.rb
。然后调用第17行的:
remove_symlinks
用于删除导入文件中存在的符号链接。此前gitlab就因为符号链接的问题爆出过多个RCE问题,因此在这里做了检查:
而extracted_files
定义在61行,这个方法用于列出解压出来的所有文件。
在ruby中,关于正则表达式的符号定义如下:
也就是说%r{.*/\.{1,2}$}
这个正则表达式最后的$
只能匹配到一行的末尾(Matches end of line),而不是整个字符串的末尾(Matches end of string)。
根据POSIX 标准,对于文件名(filename)除了slash character/
和null byte NULL
外,其余字符均可以:
所以只要创建一个名字以\n
开头的符号链接文件,就无法被extracted_files
列出。
回到版本源码对比,在测试文件file_importer_spec.rb里:
因此构建测试环境:
可以看到原本的正则表达式是无法检测到\nevil
文件的:
提供一下压缩包生成脚本:
key.pub里保存公钥。其余文件见文末附件压缩包。
创建项目project ,选择Import project
后选择Import an exported GitLab project
待导入成功后,如下图:
注意此时的项目名为test
,同时右下角有一个Remove project
,点击删除掉project,然而此时在gitlab的目录下,test
还没有被删除。
新建一个project,仍然采用Import an exported GitLab project
,然后上传第二个压缩包
第二个压缩包的内容如下,\nevil
是目录名
gitlab在解压第二个压缩包时,会尝试往目录\nevil
里写入.ssh/authorized_keys
,而由于上一步的符号链接\nevil
没有删除,所以实际写入的目录是/var/opt/gitlab/.ssh/authorized_keys
可以看到authorized_keys
已经被写入了公钥。此后用用户名git和公钥对应的私钥直接ssh连接服务器即可。
https://cwiki.apache.org/confluence/display/WW/S2-057
问题:
It is possible to perform a RCE attack when namespace value isn’t set for a result defined in underlying xml configurations and in same time, its upper action(s) configurations have no or wildcard namespace. Same possibility when using url tag which doesn’t have value and action set and in same time, its upper action(s) configurations have no or wildcard namespace.
漏洞发现者的博客: https://lgtm.com/blog/apache_struts_CVE-2018-11776
下载 https://archive.apache.org/dist/struts/2.5.16/struts-2.5.16-all.zip
IDEA中打开,修改apps/showcase/src/main/resources/struts-actionchaining.xml 为:
同时查看 org/apache/struts2/default.properties:201 ,其值为true
访问: http://localhost:8081/${(111+111)}/actionChain1.action
url变为: http://localhost:8081/222/register2.action
111+111=222 即产生了OGNL注入。
这次的漏洞可以有多种攻击向量,根据漏洞作者blog有:
以上提及的三种都属于Struts2的跳转方式。在 struts-default.xml:190(截取部分)
为清楚起见,这里解释一下strut2中对默认result对象
的处理过程。这些默认result type
都要经过 com/opensymphony/xwork2/DefaultActionInvocation.java:367 处理
首先通过result = createResult()
获取到相应的result对象。如果result不为null则执行result.execute(this);
。这个execute
方法则由具体result对象实现。
有一些具体的result对象比如下面提到的Redirect action和Postback result,会产生一个跳转地址location,并传入org/apache/struts2/result/StrutsResultSupport.java:194:
而conditionalParse
定义如下,将会执行OGNL表达式。
所以可以看到重点是StrutsResultSupport中conditionalParse(location, invocation)
的location变量。
接下来部分就关注三种result-type的具体实现和具体攻击点。
apps/showcase/src/main/resources/struts-actionchaining.xml 中注意<result>
标签中<type>
为redirectAction
:
redirectAction
对应的处理类为org.apache.struts2.result.ServletActionRedirectResult
在 com/opensymphony/xwork2/DefaultActionInvocation.java:368
跟入redirectAction
的execute
方法即 org/apache/struts2/result/ServletActionRedirectResult.java:160
由于在配置xml时没有指定naPmespace,所以这里的namespace为null,将会执行invocation.getProxy().getNamespace();
所以执行后对于result对象的namespace即为/${(111+111)}
。
同一函数中继续执行 172行
ActionMapping
生成如下,this.namespace
值赋为/${(111+111)}
:
跟入getUriFromActionMapping
:
handleNamespace
处理结果如下:
当函数返回,tmpLocation
值为/${(111+111)}/register2.action
,然后通过setLocation(tmpLocation)
使得location变量值为/${(111+111)}/register2.action
,从而最终造成OGNL注入。
apps/showcase/src/main/resources/struts-actionchaining.xml 中注意<result>
标签中<type>
为chain
:
同样会先经过result = createResult()
,然后调用result.execute(this);
。这会进入到 com/opensymphony/xwork2/ActionChainResult.java:203
由于没有设定namespace
,所以通过invocation.getProxy().getNamespace()
使得this.namespace
值为/${(111+111)}
。然后调用了String finalNamespace = TextParseUtil.translateVariables(namespace, stack);
对namespace进行OGNL解析。如下
apps/showcase/src/main/resources/struts-actionchaining.xml 中注意<result>
标签中<type>
为postback
:
经过result = createResult()
,跟入定位到postback
这个result对象的处理方法,在 org/apache/struts2/result/PostbackResult.java:113
跟入makePostbackUri1
,在org/apache/struts2/result/PostbackResult.java:129
获取到namespace
值为/${(111+111)}
。跟入actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null))
,其具体执行过程如攻击点一[Redirect action]提到的那样,设置namespace等参数,然后从getUriFromActionMapping
中返回uri。最后组装的postbackUri为/${(111+111)}/register2.action
回到前面的execute
中通过setLocation(postbackUri)
设置了location变量:
此后location变量传入,造成OGNL表达式注入
该漏洞由安全研究人员 Orange Tsai发现。漏洞公告来自 https://groups.google.com/forum/#!topic/rubyonrails-security/ft_J--l55fM
影响面: development servers,且开启了 config.assets.compile
本地安装好ruby和rails。以ruby 2.4.4 ,rails v5.0.7为例:
此时blog这个rails项目使用的sprockets版本是3.7.2(fixed)。修改blog目录下的Gemfile.lock第122行:
修改配置文件 config/environments/production.rb
:
在blog目录下执行
payload:
win平台:
linux平台
注:为明白起见,许多分析直接写在代码注释部分,请留意。
问题出在sprockets
,它用来检查 JavaScript 文件的相互依赖关系,用以优化网页中引入的js文件,以避免加载不必要的js文件。当访问如http://127.0.0.1:3000/assets/foo.js
时,会进入server.rb:
forbidden_request
用来对path进行检查,是否包含..
以防止路径穿越,是否是绝对路径:
如果请求中包含..
即返回真,然后返回forbidden_response(env)信息。
回到call函数,进入find_asset(path, options)
,在 lib/ruby/gems/2.4.0/gems/sprockets-3.7.1/lib/sprockets/base.rb:63:
跟进load
,在 lib/ruby/gems/2.4.0/gems/sprockets-3.7.1/lib/sprockets/loader.rb:32 。以请求GET /assets/file:%2f%2f//C:/chybeta/blog/app/assets/config/%252e%252e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2f%252e%2e%2fWindows/win.ini
为例,其一步步的解析过程见下注释:
跟入UnloadedAsset.new
跟入URIUtils.parse_asset_uri
主要是进行了两个检查:文件是否存在和是否在合规目录里。主要关注第二个检测。其中config[:paths]
是允许的路径,而unloaded.filename
是请求的路径文件名。跟入 lib/ruby/gems/2.4.0/gems/sprockets-3.7.2/lib/sprockets/path_utils.rb:120:
继续跟入split_subpath
, lib/ruby/gems/2.4.0/gems/sprockets-3.7.2/lib/sprockets/path_utils.rb:103。假设上面传入的path参数是``。
通过检查后,在load_from_unloaded
末尾即进行了读取等操作,从而通过路径穿越造成任意文件读取。
如果文件以.erb
结尾,则会直接执行:
在server.rb中,增加关键字过滤://
。
Opentsdb是基于Hbase的分布式的,可伸缩的时间序列数据库。官方提供了一个web界面来提供对查询数据进行可视化分析,其背后的绘图由Gnuplot支持。其Github地址为: https://github.com/OpenTSDB/opentsdb 。在某些版本(比如2.3.0,以下分析以2.3.0版本为例)中,其提供的Web接口存在远程命令执行漏洞,一旦利用成功将以root权限执行。分析见下。
在opentsdb中,默认情况下tsd.core.enable_ui
开启,允许通过http来进行rpc调用。当访问时/q?xx=xxx
时,对应的rpc接口即GraphHandler
。见 src/tsd/RpcManager.java:297:
在 src/tsd/GraphHandler.java:108 execute中
跟入 doGraph
其中接受参数在
src/tsd/GraphHandler.java:198 doGraph 中:
从请求中获取对应值并设置plot参数在setPlotParams(query, plot);
中完成:
为方便起见,整理一下http请求参数、java代码、plot参数的对应关系。有一些参数经过了stringify
,用于后续的JSON格式的转换。经过stringify
的参数都会被双引号包含(见下面的代码),难以后续逃逸使用。还有一些参数直接被设定为空值。这些参数对应如下:
http请求参数 | Java代码 | plot参数 |
---|---|---|
ylabel | put(“ylabel”, stringify(value)) | ylabel |
y2label | put(“y2label”, stringify(value)) | y2label |
yformat | put(“format y”, stringify(value)) | format y |
y2format | put(“format y2”, stringify(value)) | format y2 |
xformat | put(“format x”, stringify(value)) | format x |
ylog | put(“logscale y”, “”) | logscale y |
y2log | put(“logscale y2”, “”) | logscale y2 |
title | put(“title”, stringify(value)) | title |
stringify
定义在 src/tsd/GraphHandler.java:658 :
escapeJson
定义在 src/tsd/HttpQuery.java:471 中,主要对一些特殊字符进行转义:
还有一些参数并没有经过转义等,如下表
http请求参数 | Java代码 | plot参数 |
---|---|---|
yrange | put(“yrange”, value) | yrange |
y2range | put(“y2range”, value) | y2range |
key | put(“key”, value) | key |
bgcolor | put(“bgcolor”, value) | bgcolor |
fgcolor | put(“fgcolor”, value) | fgcolor |
smooth | put(“smooth”, value) | smooth |
style | put(“style”, value) | style |
在完成参数设置后,创建了一个RunGnuplot
对象,其中前面解析到的参数即对应的写入到了plot
属性中
|
|
在doGraph
的最后执行了execGnuplot(rungnuplot, query);
,即src/tsd/GraphHandler.java:256
这边RunGnuplot
实现了Runnable
接口,因此当线程开始执行时调用的是RunGnuplot
的run
方法:
跟入execute()
:
跟入runGnuplot
,位置在src/tsd/GraphHandler.java:758
dumpToFiles
方法定义在src/graph/Plot.java:196
:
跟入writeGnuplotScript(basepath, datafiles)
,这个方法会生成真正的Gnuplot脚本,方便起见我往里面加了注释
在完成了plot.dumpToFiles(basepath);
后,开启子进程运行生成的Gnuplot脚本:
而gnuplot中允许使用反引号来执行sh命令,
交互模式下:
脚本执行模式下:
因此我们可以通过远程控制特定的参数,使得Gnuplot在运行脚本时远程命令执行。支持远程命令执行的可控参数如下:
http请求参数 | Java代码 | plot参数 |
---|---|---|
y2range | put(“y2range”, value) | y2range |
key | put(“key”, value) | key |
bgcolor | put(“bgcolor”, value) | bgcolor |
fgcolor | put(“fgcolor”, value) | fgcolor |
smooth | put(“smooth”, value) | smooth |
style | put(“style”, value) | style |
o | 省略 | 省略 |
先查出可以使用的metrics
发包,在参数位置处填入payload。
|
|
漏洞影响版本:
测试环境: win平台
通过查找commit记录可知需要将其检出至 29ca81dd59c255ad633f1bd86cf1be40a5f02c64之前
检查core/pom.xml
的第41行,确保版本为1.250
然后命令行下编译war包
在jenkins\war\target
目录下获得编译好的jenkins.war
,同目录下启动:
在管理员登陆(有cookie)的情况下
在没有登陆(未授权,cookie清空)的情况下,只有当管理员开启了allow anonymous read access
的时候,才能实现任意文件读取,否则仍需登陆。
开启:
未开启:
而在linux下利用条件会更加苛刻,见后文。
以payload为例,请求的url为/plugin/credentials/.ini
。而在hudson/Plugin.java:227
doDynamic
函数用于处理类似/plugin/xx
的请求,serveLocalizedFile
在stapler-1.250-sources.jar!/org/kohsuke/stapler/ResponseImpl.java
第209行左右:
先看最里面的request.getLocale()
,然后再来分析stapler.selectResourceByLocale()
。
跟入request.getLocale()
,至jetty-server-9.2.15.v20160210-sources.jar!/org/eclipse/jetty/server/Request.java:692
:
这里用于处理HTTP请求中的Accept-Language
头部。比如zh-cn
,则会根据-
的位置被分为两部分,language
为zh
,country
为cn
,然后返回Locale(language,country)
对象。倘若不存在-
,则country
为空,language
即对应我们的payload:../../../../../../../../../../../../windows/win
,则此时返回一个Locale(language,"")
返回后即进入selectResourceByLocale(URL url, Locale locale)
,这里的locale
参数即上一步返回的locale对象。
urlLocaleSelector
对象的声明见stapler-1.250-sources.jar!/org/kohsuke/stapler/Stapler.java:390
:
在stapler-1.250-sources.jar!/org/kohsuke/stapler/Stapler.java:324
实现了LocaleDrivenResourceSelector
类的open
方法:
先看看开头的注释,这段代码本意是想根据对应的语言(Accept-Language)来返回不同的文件,比如在ja
的条件下请求foo.html
,则相当于去请求foo_ja.html
,这个过程会先把foo.html
分成两部分:文件名foo
和扩展名.html
,然后根据具体的语言/国家来组合成最终的文件名。
结合payload来看,我们请求的url为/plugin/credentials/.ini
,则base
为空,扩展名(ext变量)即为.ini
,然后通过一系列的尝试openURL,在此例中即最后一个情形con = openURL(map(base+'_'+ locale.getLanguage()+ext));
,会去请求_../../../../../../../../../../../../windows/win.ini
,尽管目录_..
并不存在,但在win下可以直接通过路径穿越来绕过。但在linux,则需要一个带有_
的目录来想办法绕过。
Jenkins官方修改了pom.xml,同时增加一个测试用例文件。真正的补丁在stapler
这个web框架中,见commit记录: https://github.com/stapler/stapler/commit/8e9679b08c36a2f0cf2a81855d5e04e2ed2ac2b3 :
对从locale
取出的language
,country
,variant
均做了正则的校验,只允许字母数字以及特定格式的出现。在接下来的openUrl中,根据三种变量的不同检查情况来调用不同的请求,从而防止了路径穿越漏洞造成的任意文件读取漏洞。