lightless blog - Python
type: channel
link: https://lightless.me/category/Python
description: Python
items: Get Python Code From PYINSTALLER
items: CVE-2017-17522 Python webbrowser.py 命令执行漏洞分析
items: Python协程之从放弃到死亡再到重生
items: Python协程从零开始到放弃
items: Ansible使用总结
items: 解决MySQLdb的数据库连接超时问题
items: python多线程
items: Python中Pickle的代码执行
items: [只是为了交作业] 多线程端口扫描器
items: 暴力跑子域名

Get Python Code From PYINSTALLER
type: item
date: 2018-01-02T12:53:51+00:00
description: 0x00 pyinstaller 今天wilson师傅说要把他的fenghuang-scan打包成可执行文件，据说用了一个叫做pyinstaller的东西，很是好奇。以前只是听说过这种东西，于是我也搞了一个试试看，不过我主要是想看看能不能将打包好的源码还原出来。 我们先来看看这个东西是怎么工作的吧，文档中有对这一部分进行介绍。https://pyinstaller.readthedocs.io/en/stable/operating-mode.html PyInstaller reads a Python script written by you. It analyzes your code to discover every other module and library your script needs in order to execute. Then it collects copies of all those files – including the active Python interpreter! – and puts them with your script in a single folder, or optionally in a single executable file. 按照这个说法，pyinstaller会分析你的python源码，然后把需要用到的库以及当前的python interpreter复制一份到一个单独的文件夹中，也可以生成一个单独的可执行文件。看起来很厉害的样子。那么这单独一个文件是怎么工作的呢，文档里也有讲到：https://pyinstaller.readthedocs.io/en/stable/operating-mode.html#how-the-one-file-program-works 这里我简单讲一下，更详细的内容可以参考文档。打包成一个文件后，这个文件的核心其实是一个bootloader。执行的时候，会在临时文件夹下面创建一个_MEIXXXXX的文件夹，然后bootloader会把python脚本中用到的一些文件解压扔进去，大部分是so文件。接下来就是执行python脚本了，当程序结束之后会删除这个文件夹。 打包后的文件中不包含任何的python源代码，而是将pyc打包进去了，当然打包进去的pyc进行了一些特殊的处理，后面会讲到。 0x01 打包一个文件试试看 我这里写了个看自己IP的小程序，代码很简单： #!/usr/bin/env python # coding: utf-8 # file: myip.py from __future__ import print_function import time import requests from api import API def main(): url = API.url r = requests.get(url) print(r.content) if __name__ == "__main__": # time.sleep(120) main() #!/usr/bin/env python # coding: utf-8 # file: api.py class API(object): def __init__(self): super(API).__init__() url = "http://myip.ipip.net" 这里为了测试打包，故意写的稍微复杂了一些，拆成了两个文件。下面就试试合并成一个可执行文件： # lightless @ VM-UBUNTU in ~/program/pyinstaller [22:18:27] $ pyinstaller -F myip.py 15 INFO: PyInstaller: 3.2.1 15 INFO: Python: 2.7.12 16 INFO: Platform: Linux-4.4.0-77-generic-x86_64-with-Ubuntu-16.04-xenial 16 INFO: wrote /home/lightless/program/pyinstaller/myip.spec 19 INFO: UPX is not available. 20 INFO: Extending PYTHONPATH with paths ['/home/lightless/program/pyinstaller', '/home/lightless/program/pyinstaller'] 20 INFO: checking Analysis 24 INFO: Building because /home/lightless/prog…

CVE-2017-17522 Python webbrowser.py 命令执行漏洞分析
type: item
date: 2017-12-19T08:22:01+00:00
description: 今日惊闻Python出现了CVE，问题出在Lib/webbrowser.py模块，看描述还十分严重。 Python容易产生远程命令执行漏洞。攻击者可以利用此问题，在运行受影响应用程序的用户上下文中执行任意命令。失败的尝试可能导致拒绝服务条件。 根据CVE的描述，问题出在环境变量上。 Lib/webbrowser.py in Python through 3.6.3 does not validate strings before launching the program specified by the BROWSER environment variable, which might allow remote attackers to conduct argument-injection attacks via a crafted URL. 既然是BROWSER这个环境变量有问题，那我们就看下这个文件中哪里引用到了BROWSER这个环境变量，我这里测试的环境是Python3.6.3。在620行左右，发现了相关的代码。 可以看到BROWSER环境变量经过一系列的处理，会传入register(name, klass, instance, update_tryorder)函数。我们继续跟入GenericBrowser(cmdline)。 跟入之后，发现cmdline被直接传入了subprocess.Popen()函数，问题应当就出在这里，经过简单的单步调试后，可以发现这个cmdline其实就是BROWSER和url拼接的列表，大概就是这个样子：cmdline: [BROWSER, url]，既然如此，PoC就非常容易构造了。 import webbrowser if __name__ == '__main__': webbrowser.open(".") 在运行之前先设定环境变量export BROWSER=ls，运行后即可发现命令执行了。 分析到这里，漏洞基本上已经分析完了，我不禁多了一些思考。首先，如果我们想利用这个漏洞，那么需要控制环境变量以及一个url参数，然后还要这个服务开放在公网上才能被远程利用，从这一点上来看，这个漏洞怕是有些标题党了。 回过头来再从开发者的角度去思考一下，为啥要这样写代码，我觉得初衷可能是方便用户使用自定义的浏览器来打开URL（对这个webbrowser功能不熟悉的同学可以先看看官方文档的使用方法），所以势必要让用户来提供浏览器的名称，而开发者又无法预知用户会使用什么奇葩的浏览器，所以并没有办法进行有效的过滤。 最后一处用户提供的URL部分，开发者也并没有判断这个URL是否合法，或者说最起码看起来是个正常URL的样子，传入一个.也是可以正常运行的，至少说来这个地方应该是不严谨的吧，挺好奇官方会如何修复这个问题的，也没准最后修都不修了呢23333

Python协程之从放弃到死亡再到重生
type: item
date: 2017-10-12T15:15:00+00:00
description: 前面一篇关于Python协程的文章中，提到了python中协程的一些历史以及一些简单的用法，这些用法放在实际开发中可能不太够，我在后面的编写中就踩到了不少的坑，这里就回应上篇文章中末尾说的，再写一篇文章来梳理一下这些问题。时隔很久，终于还是磨磨唧唧的把第二篇写出来了。本来还准备有第三篇重生篇的，但是以防拖稿，合并到一起来写吧。 0x00 又是生成器？ 我们已经知道了，生成器可以作为协程来使用，但是很多细节问题尚未明确，我们先从最简单的一个协程开始。（后面会出现"生成器"和"协程"两个词混用的问题，可以都理解为"作为协程使用的生成器"） def easy_coroutine(): print("Start!") x = yield print("End! x: {}".format(x)) 这就是最简单的协程了，如果想使用这个协程，看起来还是有点麻烦的： # lightless @ LL-DESKTOP in C:\Users\lightless\Desktop [20:40:27] $ python -i .\ez_coroutine.py >>> c = easy_coroutine() >>> c <generator object easy_coroutine at 0x0000016AD411E570> >>> next(c) Start! >>> c.send(666) End! x: 666 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> 创建协程的方式与创建生成器一样，通过调用函数的方法获取到一个生成器对象。紧接着调用next()方法来启动生成器，这一步也称为prime，有些文章会把这个东西翻译成"预激"，即让协程开始执行到第一个yield表达式的位置。（为了方便下文都称为预激了） 下面又调用了c.send(666)，调用者方法后，x = yield表达式就会得到666这个值，然后协程恢复运行，直到协程结束或者遇到另外的yield表达式为止。当然这里最后会抛出一个StopIteration异常，因为代码执行完毕了，这就是生成器的标准行为，不用在意。 在这个执行过程中，会发现我们的easy_coroutine()经历的多个状态，并非总是一直处于RUNNING的状态。事实上，协程的状态有四种：GEN_CREATED，GEN_RUNNING，GEN_SUSPENDED，GEN_CLOSED。这四种状态分别是等待开始执行，正在执行，停在了yield表达式位置，执行结束。如果你想获取某个协程的当前状态，可以通过inspect.getgeneratorstate函数获取。 额外说明一点，send()方法的参数会传递给协程，作为yield表达式的值，所以只有当协程处于GEN_SUSPENDED的状态时，才可以调用send()方法传递值。如果协程还没有prime，那么可以通过调用send(None)来预激协程。啥，你问为什么不能通过给尚未prime的协程调用send(233)来预激协程，如果你试过的话就会收到一个TypeError。 每次预激协程的时候都需要调用一遍next()函数，很麻烦，而且很容易忘掉，一旦忘记预激协程，后果也是十分严重的，因为没有被预激的协程没有任何作用。有没有简单的方法让Python自动预激协程呢？有的，函数装饰器就可以做到这一点，于是我们可以自己封装一套来让其自动预激！ def prime_coroutine(func): @functools.wraps(func) def prime(*args, **kwargs): t = func(*args, **kwargs) next(t) return t return prime 代码十分简单，如果看不懂的话，建议先去学习一下Python的装饰器部分。可能各位很容易想到之前提到过的一种装饰器，在Python3.4以后，标准库中新增了asyncio.coroutine装饰器，用于修饰协程函数，但是非常遗憾，这个装饰器并不会预激协程，而通过yield from语法调用一个协程时，会自动预激。所以可以通过asyncio.coroutine和yield from配合使用，实际上我们也是这样使用的。 这里有人会问，如果在写成中抛出了异常怎么办，是不是和普通函数的异常一样？当协程中出现异常时，而协程本身又没有捕获处理，那么这个异常会"冒泡"至协程的调用方，即next()或send()函数的调用方。如果协程已经抛出了异常，再尝试去调用它，会得到一个StopIteration异常。于是有人发现了可以通过让协程抛出异常的方式终止一个协程的运…

Python协程从零开始到放弃
type: item
date: 2017-07-20T15:20:00+00:00
description: 0x00 前言 很久以前就听说Python的async/await很厉害，但是直到现在都没有用过，一直都在用多线程模型来解决各种问题。最近看到隔壁的Go又很火，所以决定花时间研究下Python协程相关的内容，终于在翻阅了一裤衩的资料之后有了一些理解。 0x01 起：一切从生成器开始 以往在Python开发中，如果需要进行并发编程，通常是使用多线程/多进程模型实现的。由于GIL的存在，多线程对于计算密集型的任务并不十分友好，而对于IO密集型任务，可以在等待IO的时候进行线程调度，让出GIL，实现『假并发』。 当然对于IO密集型的任务另外一种选择就是协程，协程其实是运行在单个线程中的，避免了多线程模型中的线程上下文切换，减少了很大的开销。为了理解协程、async/await、asyncio，我们要从最古老的生成器开始。 回顾Python的历史，生成器这个概念第一次被提出的时候是在PEP 255中被提出的，当时的Python版本为Python2.2。我们都知道range()函数，现在考虑一下我们来编写一个自己的range()函数，最直接最容易想到的方法也许是这样： def my_range(max_number): sequence = [] index = 0 while index < max_number: sequence.append(index) index += 1 return sequence 当你想创建一个很小的序列的时候，例如创建从0到100这样的列表，似乎没什么问题。但是如果想创建一个从0到999999999这么大的列表的话，就必须要创建一个完整的，长度是999999999的列表，这个行为非常占用内存。于是就有了生成器，用生成器来改写这个函数的话，会是下面这个样子： def lazy_range(max_number): index = 0 while index < max_number: yield index index += 1 当函数执行遇到yield的时候，会暂停执行。这样只需在内存中维护可以存储一个整数的内存空间就可以了。如果对生成器/迭代器不理解的话，可以参考Stack Overflow上的这篇高票回答：传送门 0x02 承：协程诞生 到这里可能还和协程没什么关系，但是实际上这已经是Python协程的雏形了，我们来看看维基上对于协程的定义： Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. 从某些角度来理解，协程其实就是一个可以暂停执行的函数，并且可以恢复继续执行。那么yield已经可以暂停执行了，如果在暂停后有办法把一些value发回到暂停执行的函数中，那么Python就有了『协程』。于是在PEP 342中，添加了“把东西发回已经暂停的生成器中”的方法，这个方法就是send()，并且在Python2.5中得到了实现。利用这个特性我们继续改写range()函数： def smart_range(max_number): index = 0 while index < max_number: jump = yield index if jump is None: jump = 1 index += jump 就这样，整个生成器的部分似乎已经进入了stable的状态，但是在Python3.3中，这个情况发生了改变。在PEP 380中，为Python3.3添加了yield from，这个东西可以让你从迭代器中返回任何值（这里用的是迭代器，因为生成器也是一种迭代器），也可以让你重构生成器，我们来看这个例子： def lazy_range(max_number): index = 0 def gratuitous_refactor(): while index < max_number: yield index index += 1 yield from gratuitous_refactor() 这个特性也可以让生成器进行串联，使数据在多个生成器中进行传递。历史发展到这里，协程的出现似乎已经就差一步了，或者这里说是异步编程更恰当。在Python3.4中加入了asyncio库，使Python获得了事件循环的特性（关于事件循环的内容这里不再赘述）。asyncio + 生成器已经达到了异步编程的条件，在Python3.4中，我们就可以这样实现一个异步的模型： impo…

Ansible使用总结
type: item
date: 2016-11-13T13:29:18+00:00
description: 什么是Ansible Ansible是一种集成IT系统的配置管理，应用部署，执行特定任务的开源平台。主要基于Paramiko和PyYAML两个模块。 安装Ansible 这里是在CentOS7上安装使用的，首先要开启epel源： # yum install -y epel-release 配置公私钥证书登录 # ssh-kegen -t rsa -C "lightless@foxmail.com" # ssh-copy-id -i /root/.ssh/id_rsa.pub root@1.1.1.1 # ssh-copy-id -i /root/.ssh/id_rsa.pub root@2.2.2.2 定义主机组 默认的配置文件在/etc/ansible/hosts 有点像ini的格式： # This is the default ansible 'hosts' file. # # It should live in /etc/ansible/hosts # # - Comments begin with the '#' character # - Blank lines are ignored # - Groups of hosts are delimited by [header] elements # - You can enter hostnames or ip addresses # - A hostname/ip can be a member of multiple groups # Ex 1: Ungrouped hosts, specify before any group headers. ## green.example.com ## blue.example.com ## 192.168.100.1 ## 192.168.100.10 # Ex 2: A collection of hosts belonging to the 'webservers' group ## [webservers] ## alpha.example.org ## beta.example.org ## 192.168.1.100 ## 192.168.1.110 # If you have multiple hosts following a pattern you can specify # them like this: ## www[001:006].example.com # Ex 3: A collection of database servers in the 'dbservers' group ## [dbservers] ## ## db01.intranet.mydomain.net ## db02.intranet.mydomain.net ## 10.25.1.56 ## 10.25.1.57 # Here's another example of host ranges, this time there are no # leading 0s: ## db-[99:101]-node.example.com 默认的配置文件中提供了一些例子，这里说一些特殊的 别名 jumpserver ansible_ssh_port=22 ansible_ssh_host=1.1.1.1 这个是为主机定义一个别名，名称叫jumpserver，后面定义了host和port，除此之外还有一些保留的变量名： ansible_ssh_host ansible_ssh_port ansible_ssh_user ansible_ssh_pass ansible_connection, 连接类型，可以是local、ssh、或者是paramiko ansible_ssh_privite_key_file ansible_*_interpreter， 指定采用非python的其他脚本语言，例如ruby、perl等 主机变量 主机可以指定变量，以便提供给playbooks使用 host1 http_port=80 maxRequestsPerChild=100 host2 http_port=8080 maxRequestsPerChild=200 定义组变量 组变量就是变量的作用域会覆盖组所有成员 [atlanta] host1 host2 [atlanta:vars] ntp_server=ntp.atlanta.example.com proxy=proxy.atlanta.example.com 此外还有嵌套组，但是只在ansible-playbook中生效。 分离主机与数据 /etc/ansible/group_vars…

解决MySQLdb的数据库连接超时问题
type: item
date: 2016-10-28T06:11:00+00:00
description: 今天干活的时候把代码里的数据库连接全部改成了长连接，然后就出现了奇怪的问题。 (2013, 'Lost connection to MySQL server during query') 应该是数据库连接的空闲时间过长，导致了连接丢失。没办法只能自己封装一遍MySQLdb，在每次执行前ping一下，并且重新连接。 #!/usr/bin/env python2 # coding: utf-8 #file: ConnectDB.py import socket import MySQLdb __author__ = "lightless" __email__ = "root@lightless.me" class DB(object): """ 对MySQLdb重新封装一遍 query方法检测超时问题 Fuck MySQLdb!!! """ def __init__(self, host, port, database, username, password, charset="utf8", autocommit=True): super(DB, self).__init__() self.db = None self.cursor = None self.result = None self.execute_result = None self.host = host self.database = database self.port = port self.username = username self.password = password self.charset = charset self.autocommit = autocommit def __del__(self): self.db.close() del self.db del self.cursor def __connect(self): try: self.db = MySQLdb.connect( host=self.host, port=int(self.port), user=self.username, passwd=self.password, db=self.database, charset=self.charset, autocommit=self.autocommit ) self.cursor = self.db.cursor() except MySQLdb.MySQLError as e: print "Error Message:" print e def connect(self): self.__connect() def __query(self, sql, commit=False): self.db.ping(True) self.execute_result = self.cursor.execute(sql) if commit: self.db.commit() self.result = self.cursor.fetchall() return self.result def query(self, sql, commit=False): try: if self.cursor is None: self.__connect() return self.__query(sql, commit) except MySQLdb.OperationalError: # Mysql Connection Lost self.__connect() try: return self.__query(sql, commit) except MySQLdb.MySQLError as e: print "Error Message:" print e 这样问题就解决了。

python多线程
type: item
date: 2016-05-12T04:49:00+00:00
description: 多线程创建 def target(): print 'current threading: %s is running' % threading.current_thread().name time.sleep(1) print 'current threading: %s is ended' % threading.current_thread().name print 'current threading: %s is running' % threading.current_thread().name t = threading.Thread(target=target) t.setDaemon(True) t.start() t.join() print 'current threading: %s is ended' % threading.current_thread().name t.start() 启动线程 t.join() 阻塞当前线程，即使当前线程已经结束，也不会退出。上例中，thread-1结束后主线程才会结束。 如果不加join语句，主线程不会等待子线程结束后再退出，而是立即退出，但并不会杀死子线程。 t.setDaemon(True) 设置这个之后，如果不加join，那么主线程结束后会立即退出，并立即杀死子线程。 可以使用join(int)等待一段时间，那么主线程会等待一段时间再退出。 线程锁 lock = threading.Lock() 在需要的时候acquire()，使用后通过release()释放。 lock.acquire() try: a += 3 finally: lock.release() ThreadLocal 线程局部变量，Flask基于此原理。 local = threading.local() def func(name): local.name = name print "%s in %s" % (local.name, threading.current_thread().name) t1 = threading.Thread(target=func, args=('xiaoming',)) t2 = threading.Thread(target=func, args=('xiaohong',)) t1.start() t1.join() t2.start() t2.join() map实现多线程 urls = ['http://www.baidu.com','http://www.sina.com','http://www.qq.com'] results=map(urllib2.urlopen,urls) map函数负责将线程分给不同的CPU。 python中，有两个标准库包含map函数，分别是multiprocessing和multiprocessing.dummy。 区别：multiprocessing作用于进程，dummy作用于线程。 import urllib2 from multiprocessing.dummy import Pool as ThreadPool urls = ['http://www.baidu.com','http://www.sina.com','http://www.qq.com'] pool = ThreadPool() results = pool.map(urllib2.urlopen,urls) print results pool.close() pool.join() print 'main ended' pool = ThreadPool() 创建线程池，默认值为CPU的核心数。可以自己手动指定，该数值并非越大越好，因为切换线程时会很消耗资源。 result = pool.map(urllib2.urlopen, urls) 将不同的url传递给不同的线程，并把执行结果返回到result中。 python多线程缺陷 python中存在一个GIL，全局解释器锁，该锁的存在保证同一时间只有一个线程执行任务，也就是说，python的多线程并非真正的并发，而是交替执行。 参考资料 https://segmentfault.com/a/1190000000414339 http://python.jobbole.com/85050/ http://zhuoqiang.me/python-thread-gil-and-ctypes.html

Python中Pickle的代码执行
type: item
date: 2015-07-03T06:23:00+00:00
description: 在python中，有时候会遇到序列化和反序列化的问题，这时候用到了Pickle/CPickle这个库，他的作用就是对给定的内容进行序列化以及反序列化。 但是在反序列化的时候，同样会出现类似于PHP中的wakeup魔术方法，在返序列化的时候会被自动执行。那就是类里面的__reduce__方法。 代码如下： import cPickle import subprocess class A(object): def __reduce__(self): return (subprocess.Popen, (('calc.exe',),)) x = cPickle.dumps(A()) cPickle.loads(x) 所以在开发过程中，千万不要将外部传输过来不可信的字符串，未经验证的就进行反序列化，这样往往会导致很严重的安全问题。

[只是为了交作业] 多线程端口扫描器
type: item
date: 2015-05-21T03:15:14+00:00
description: 多线程的端口扫描器，目前只是半成品，只是为了交作业。 代码见github：https://github.com/LiGhT1EsS/portScan 很简单，没啥技术含量，体力活。

暴力跑子域名
type: item
date: 2015-02-08T07:31:00+00:00
description: 近日和大帅比琢磨着搞个跑子域名的东西。。多番实践之下，写出来个残次品，勉强能用吧。 大概思路就是：先设置一个生成前缀的线程，不停的生成前缀，然后放入队列中。若干个工作线程不停的从队列中取出前缀然后组合成URL判断是否存在这个子域名，如果存在的话放到另一个队列中，最后设置一个线程负责从2号队列中取出存在的域名，并写入文件。 判断这个子域名是否存在的思路用了getaddrinfo()和返回的状态码，如果调用getaddrinfo()没有抛出异常，并且ip也不是1.1.1.1，并且HTTP状态码不是404的话，就认为这个域名是存在的。 Github: https://github.com/LiGhT1EsS/GetSubDomain/ 代码如下： #!/usr/bin/python #coding:utf-8 import socket import itertools import string import threading import time import sys, getopt, os import random from Queue import Queue import urllib WorkQueue = Queue() LogQueue = Queue() domainLock = threading.Lock() thread_list = [] MakeFinished = 0 WriteFinished = 0 ScanFinished = 0 class WriteLogFile(threading.Thread): def __init__(self, url): threading.Thread.__init__(self) self.url = url def run(self): global LogQueue global WriteFinished global ScanFinished filename = self.url + ".txt" f = open(filename, "w+") while True: for i in thread_list: if not i.is_alive(): ScanFinished = 1 elif i.is_alive(): ScanFinished = 0 #print ScanFinished, LogQueue.empty() if ScanFinished == 1 and LogQueue.empty(): break data = LogQueue.get() if data == "Nothing Found!": continue f.write(data + "\n") f.close() WriteFinished = 1 def stop(self): global WriteFinished WriteFinished = 1 try: f.close() except Exception, e: pass class MakeDomainPrefix(threading.Thread): """docstring for MakeDomainPrefix""" def __init__(self, maxlen): threading.Thread.__init__(self) self.maxlen = maxlen def run(self): global WorkQueue global MakeFinished # 生产子域名并放到队列里 for i in range(1, int(self.maxlen)+1): for j in itertools.product(string.ascii_lowercase + string.digits, repeat=i): line = ''.join(str(x) for x in j) WorkQueue.put(line) #print "put " + line MakeFinished = 1 class FindSubdomain(threading.Thread): def __init__(self, url): threading.Thread.__init__(self) self.url = url def run(self): # 从队列中取出并进行操作 global WorkQueue global LogQueue global MakeFinished global g_http #print "thread start!" # print WorkQueue.qsize() # print MakeFinished while Tr…