PIPの脆弱性管理
現在システム全体の残留脆弱性の一覧化の方法を整理しています。
その際の基礎調査として、pip編です。
Vulsなどでは利用しているOSの標準レポジトリから導入したpackage情報はきれいに引き抜いてくれるのですが、pipで導入したものや、非標準のレポジトリ(各ソフトウェアベンダが独自に用意しているレポジトリ)から導入したパッケージ分はサーチしてくれません(2023/4現在)。
ということで、pip用の調べ方を整理します。
PIPの脆弱性検索方針
pip freezeなどで得られる情報から、なんとかモジュール情報をCPEに変換して検索する、というのを考えていました。
一方、ググっていたら独自にPVEという脆弱性番号を裁判してガッツリ活動されているpyup社のSafetyというツールも見つけました。
この両者を比較しながら、今回の残留是弱製情報の一覧化に使う方法を整理していこうと思います。
力づくでCPE化するアプローチ
今回の一連の調査の中で、cve-searchのコードを見ている際に、CPEのリストをNVDから持ってきて、CPEのベンダ名やプロダクト名の文字列とrequirements.txtのパッケージ名の文字列をマッチングして引っこ抜こうとする、という実装を見かけました(バグがあって機能しないのですが、、)
ググってみた感じでエレガントな方法もなさそうですし、、その方法を踏襲してやってみようかなと思います。
今回の調査はVulsを主軸にして実施しようと考えていますので、、go-cpe-dictionaryでダウンロードしてきたCPE情報に対して検索をかけるようにします。
こんな感じ。
#!/usr/bin/env python3
import sys
import sqlite3
import re
import requirements
db_name = './cpe.sqlite3'
connection = sqlite3.connect(db_name)
def _search(vendor, product, version, ambiguous=False):
version = re.escape(version)
sql_query = 'select cpe_fs from categorized_cpes'
wheres = []
if vendor:
if ambiguous:
wheres.append("vendor like '%{}%'".format(vendor))
else:
wheres.append("vendor like '{}'".format(vendor))
if product:
if ambiguous:
wheres.append("product like '%{}%'".format(product))
else:
wheres.append("product like '{}'".format(product))
if version:
wheres.append("version like '{}%'".format(version))
if len(wheres) == 0:
print("Search Condition None, None, None.")
return
sql_query += " where " + " and ".join(wheres)
cursor = connection.cursor()
cursor.execute(sql_query)
return(cursor.fetchall())
def search(vendor, product, version):
vs = version.split(".")
for i in range(len(vs), -1, -1):
version = ".".join(vs[:i])
result = _search(vendor, product, version)
if result != []:
return result
for i in range(len(vs), -1, -1):
version = ".".join(vs[:i])
result = _search(vendor, product, version, ambiguous=True)
if result != []:
return result
return []
# Main
if sys.stdin.isatty():
print("Run like follows.")
print("pip3 freeze | ./pip2cpe.py")
exit(1)
for req in requirements.parse(sys.stdin):
lib = req.name
specs = req.specs
print(req)
for spec in specs:
if not spec[0] in ['==', '===']:
print("Please use freezed requirements.")
for candidate in search(None, lib, spec[1]):
print(" {}".format(candidate))
バージョンの部分について、ちょっと一ひねりしています。
バージョンの記載はNVDの各CPEの登録でも結構揺らいでいるので、pip-freezeで取得したバージョンの「文字列」がそのまま使えるか胡散臭いので、広めに検索できるようにしてます。
また、プロダクト名(モジュール名)も怪しいので、検索結果がなければ検索単語よりも長いものも入れて検索しています。
現状実装してませんが、 「zope.interface」は「zope」だったらCPEがヒットする、というケースもあるので、逆にモジュール名を細切れにして検索するというのも必要かもですが、、
まぁそこら編は必要になってから考えます。
実際に試すと、pipにあるけどCPEがヒットしないものも多数あります。
手で追加調査してみましたが、それらについては、CVE情報が見つかったことがなく、NVDの配布している情報に該当がない、というもののようです。
そもそもが雑な方法なのでこれで網羅しきれているか怪しいですし、該当ソフトの作成者がCVE採番しないで対処してバージョンアップしている、ということもある気がしますので、該当がないから安心して今のバージョン使おうではなく、確証持てないからアプデートしておこう、のほうが健全な考え方だとは思うのですが。
上記の条件で、CPE一覧を作って、vulsでサーチしてみます。
結果は後述。
Safety
https://github.com/pyupio/safety
PyUp社が独自に、PVEという識別子ベースでpythonモジュールなどの脆弱性をDB化してくれているらしく、そこで引っ掛けるようです。
pipで導入したら、実行。
$ safety check -o json
比較してみる
手動でCPEを推測してみて、CPEベースでVulsで検索してみた結果で比較してみます。
今回の前提条件。
導入中のpip
今回の調査で使っている環境をそのまま使用。
$ pip3 freeze
appdirs==1.4.4
attrs==21.2.0
Automat==20.2.0
Babel==2.8.0
backcall==0.2.0
bcrypt==3.2.0
beautifulsoup4==4.10.0
beniget==0.4.1
blinker==1.4
Brotli==1.0.9
certifi==2020.6.20
chardet==4.0.0
click==8.0.3
cloud-init==22.4.2
colorama==0.4.4
command-not-found==0.3
configobj==5.0.6
constantly==15.1.0
cryptography==3.4.8
cycler==0.11.0
dbus-python==1.2.18
decorator==4.4.2
distro==1.7.0
distro-info===1.1build1
dparse==0.6.2
fonttools==4.29.1
fs==2.4.12
gast==0.5.2
html5lib==1.1
httplib2==0.20.2
hyperlink==21.0.0
idna==3.3
importlib-metadata==4.6.4
incremental==21.3.0
ipdb==0.13.13
ipython==7.31.1
jedi==0.18.0
jeepney==0.7.1
Jinja2==3.0.3
jsonpatch==1.32
jsonpointer==2.0
jsonschema==3.2.0
keyring==23.5.0
kiwisolver==1.3.2
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
lxml==4.8.0
lz4==3.1.3+dfsg
MarkupSafe==2.0.1
matplotlib==3.5.1
matplotlib-inline==0.1.3
more-itertools==8.10.0
mpmath==0.0.0
netifaces==0.11.0
numpy==1.21.5
oauthlib==3.2.0
olefile==0.46
packaging==21.3
parso==0.8.1
pexpect==4.8.0
pickleshare==0.7.5
Pillow==9.0.1
ply==3.11
prompt-toolkit==3.0.28
ptyprocess==0.7.0
pyasn1==0.4.8
pyasn1-modules==0.2.1
Pygments==2.11.2
PyGObject==3.42.1
PyHamcrest==2.0.2
PyJWT==2.3.0
pyOpenSSL==21.0.0
pyparsing==2.4.7
pyrsistent==0.18.1
pyserial==3.5
python-apt==2.4.0+ubuntu1
python-dateutil==2.8.1
python-debian===0.1.43ubuntu1
python-magic==0.4.24
pythran==0.10.0
pytz==2022.1
PyYAML==5.4.1
requests==2.25.1
requirements-parser==0.5.0
ruamel.yaml==0.17.21
ruamel.yaml.clib==0.2.7
safety==2.3.5
scipy==1.8.0
SecretStorage==3.3.1
service-identity==18.1.0
six==1.16.0
sos==4.4
soupsieve==2.3.1
ssh-import-id==5.11
sympy==1.9
systemd-python==234
toml==0.10.2
tomli==2.0.1
traitlets==5.1.1
Twisted==22.1.0
types-setuptools==67.6.0.8
ubuntu-advantage-tools==8001
ubuntu-drivers-common==0.0.0
ufoLib2==0.13.1
ufw==0.36.1
unattended-upgrades==0.1
unicodedata2==14.0.0
urllib3==1.26.5
wadllib==1.3.6
wcwidth==0.2.5
webencodings==0.5.1
xkit==0.0.0
zipp==1.0.0
zope.interface==5.4.0
手動でCPEを引っこ抜くアプローチ
上記に記載したスクリプトの実行結果から、妥当なCPEだけ残して、vulsの設定として落としこんだ結果は以下の通り。
$ cat ./config.toml
[servers]
[servers.sv01]
type = "pseudo"
cpeNames = [
"cpe:2.3:a:attrs_project:attrs:21.2.0:*:*:*:*:*:*:*",
"cpe:2.3:a:pocoo:babel:2.8.0:*:*:*:*:*:*:*",
"cpe:2.3:a:node.bcrypt.js_project:node.bcrypt.js:3.0.8:*:*:*:*:node.js:*:*",
"cpe:2.3:a:google:brotli:1.0.9:*:*:*:*:*:*:*",
"cpe:2.3:a:certifi_project:certifi:*:*:*:*:*:*:*:*",
"cpe:2.3:a:click_project:click:-:*:*:*:*:*:*:*",
"cpe:2.3:a:configobj_project:configobj:5.0.6:*:*:*:*:*:*:*",
"cpe:2.3:a:cryptography_project:cryptography:3.4.8:*:*:*:*:python:*:*",
"cpe:2.3:a:python:decorator:4.3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:amazon:open_distro:1.7.0:*:*:*:*:elasticsearch:*:*",
"cpe:2.3:a:html5lib:html5lib:1.1:*:*:*:*:*:*:*",
"cpe:2.3:a:httplib2_project:httplib2:0.19.0:*:*:*:*:python:*:*",
"cpe:2.3:a:pocoo:jinja2:*:*:*:*:*:*:*:*",
"cpe:2.3:a:jsonpointer_project:jsonpointer:2.0.0:*:*:*:*:node.js:*:*",
"cpe:2.3:a:python:keyring:*:*:*:*:*:*:*:*",
"cpe:2.3:a:lxml:lxml:*:*:*:*:*:*:*:*",
"cpe:2.3:a:lz4_project:lz4:*:*:*:*:*:*:*:*",
"cpe:2.3:a:mpmath:mpmath:*:*:*:*:*:*:*:*",
"cpe:2.3:a:numpy:numpy:*:*:*:*:*:*:*:*",
"cpe:2.3:a:oauthlib_project:oauthlib:3.2.0:*:*:*:*:*:*:*",
"cpe:2.3:a:parso_project:parso:*:*:*:*:*:*:*:*",
"cpe:2.3:a:python:pillow:9.0.1:*:*:*:*:*:*:*",
"cpe:2.3:a:pygments:pygments:*:*:*:*:*:*:*:*",
"cpe:2.3:a:pyjwt_project:pyjwt:2.3.0:*:*:*:*:*:*:*",
"cpe:2.3:a:pyopenssl:pyopenssl:*:*:*:*:*:*:*:*",
"cpe:2.3:a:pyyaml:pyyaml:5.4.1:*:*:*:*:*:*:*",
"cpe:2.3:a:python:requests:2.25.1:*:*:*:*:*:*:*",
"cpe:2.3:a:pyup:safety:*:*:*:*:*:*:*:*",
"cpe:2.3:a:scipy:scipy:*:*:*:*:*:*:*:*",
"cpe:2.3:a:sos_project:sos:4.2-20.el8_6:*:*:*:*:*:*:*",
"cpe:2.3:a:twistedmatrix:twisted:22.1.0:*:*:*:*:*:*:*",
"cpe:2.3:a:python:urllib3:1.26.5:*:*:*:*:*:*:*",
]
CPEが見つからなかったものや、適切なバージョンがなくて少し前のバージョンを選んだもの、諦めて*にしたものも多々。
で、Safety側と比較しやすいようにちょっと表示を捻りつつ出力します。
ホントはjsonで出力したのですが、現状うまくjsonで出力ができず、、、
-format-jsonのオプションをつけてはいるのですが、、
調べきれてないのですが、今回はまぁ外はないので、力技で行きます。
$ report.sh -format-json 2>/dev/null | grep "CVE-2" | sort
| CVE-2020-7689 | 7.5 | AV:N | | | | cpe:/a:node.bcrypt.js_project:node.bcrypt.js:3.0.8::~~~node.js~~ || CVE-2021-23807 | 9.8 | AV:N | POC | | | cpe:/a:jsonpointer_project:jsonpointer:2.0.0::~~~node.js~~ |
| CVE-2021-23820 | 9.8 | AV:N | POC | | | cpe:/a:jsonpointer_project:jsonpointer:2.0.0::~~~node.js~~ |
| CVE-2021-31828 | 7.1 | AV:N | | | | cpe:/a:amazon:open_distro:1.7.0::~~~elasticsearch~~ |
| CVE-2021-42771 | 7.8 | AV:L | POC | | | cpe:/a:pocoo:babel:2.8.0 |
| CVE-2022-21716 | 7.5 | AV:N | POC | | | cpe:/a:twistedmatrix:twisted:22.1.0 |
| CVE-2022-24801 | 8.1 | AV:N | | | | cpe:/a:twistedmatrix:twisted:22.1.0 |
| CVE-2022-29217 | 7.5 | AV:N | | | | cpe:/a:pyjwt_project:pyjwt:2.3.0 |
| CVE-2022-36087 | 6.5 | AV:N | POC | | | cpe:/a:oauthlib_project:oauthlib:3.2.0 |
| CVE-2022-39348 | 5.4 | AV:N | POC | | | cpe:/a:twistedmatrix:twisted:22.1.0 |
| CVE-2022-45198 | 7.5 | AV:N | | | | cpe:/a:python:pillow:9.0.1 |
| CVE-2022-45199 | 7.5 | AV:N | | | | cpe:/a:python:pillow:9.0.1 |
| CVE-2023-23931 | 6.5 | AV:N | POC | | | cpe:/a:cryptography_project:cryptography:3.4.8::~~~python~~ |
13件がヒット。
Safety
こちらは簡単ですね。
ただし、Vuls側と比較しやすいように、表示はちょっと整形します。
$ safety check -o json | jq -r '.vulnerabilities[]|[.CVE, .package_name] | @csv' | sort
"CVE-2021-34141","numpy"
"CVE-2021-41495","numpy"
"CVE-2021-41496","numpy"
"CVE-2021-42771","babel"
"CVE-2022-21716","twisted"
"CVE-2022-2309","lxml"
"CVE-2022-23491","certifi"
"CVE-2022-24801","twisted"
"CVE-2022-29217","pyjwt"
"CVE-2022-36087","oauthlib"
"CVE-2022-39348","twisted"
"CVE-2022-40897","setuptools"
"CVE-2022-40898","wheel"
"CVE-2022-45198","pillow"
"CVE-2022-45199","pillow"
"CVE-2023-23931","cryptography"
"CVE-2023-24816","ipython"
全部で17件がヒット。
比較結果
手動引っこ抜きと、Safetyを使った場合の結果を比較して、深堀していってみます。
手動CPE引っこ抜きでのみ検知したもの
1. CVE-2020-7689
cpe:2.3:a:node.bcrypt.js_project:node.bcrypt.js:3.0.8:*:*:*:*:node.js:*:*
名前が近いので一応入れておいたのですが、もとはbcrypt==3.2.0です。
名称や、9個目の要素のnode.js(9個目の要素はtarget_sw)を見るに、node用の実装の場合、、、ということでしょうか。
https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7695.pdf
pip(python)の話じゃないということで、誤検知ですね。
2. CVE-2021-23807, CVE-2021-23820
cpe:2.3:a:jsonpointer_project:jsonpointer:2.0.0:*:*:*:*:node.js:*:*
もとはjsonpointer==2.0。
これも誤検知ですね。
3. CVE-2021-31828
cpe:2.3:a:amazon:open_distro:1.7.0:*:*:*:*:elasticsearch:*:*
distro==1.7.0より。
これも第9要素がelasticsearchになっているし、NVDの記載でもAn SSRF issue in Open Distro for Elasticsearch (ODFE) とありますから、誤検知でいいですかね。
Safetyのみで検知したもの
量多いので、抜粋で。
CVE-2021-34141
vuls上ではcpe:2.3:a:numpy:numpy:*:*:*:*:*:*:*:*
で記載しているのですが、何故かヒットしません。
(バージョンドンピシャがなかったので、バージョンは*にしてます)
numpy。
今回config.tomlに使用したバージョンを*にしたCPEなら引っかかっても良さそうなのですが、、
少々深堀しましたがどうやらvulsでも引っ掛けて入るのですが、confidencesが低くいため、通常の表示で出ないようです。
reportの際に、--confidence-over=10とすれば表示にふくまれるようになり、そうするとversionを*にしているものも含めて引っこ抜いてくれるようになります。
ただし、*にしていることの弊害(現在ほんとに使っているバージョンでは解消済みの脆弱性)というケースと、Safetyでは最初から検出できているものばかりな感じです。
全部見ているわけではないのですが。
また、NVDを見ると、そっちも結構厄介なことになっています。
記載状も、ソース上も、影響範囲は1.22.0未満になっているのですが、CPEリストに1.21とかの記載がないんですね。。
NVD情報ベースで引っ掛けるのは、色々と不安が残ります。
まとめ
pipの脆弱性情報収集は、Safetyに任せちゃって良さそうですね。