SoFunction
Updated on 2024-10-30

Python Case Study of Executing JS Functions with PyExecJS Library

  In the Web penetration process of violent login scenarios and crawler crawling scenarios, often encountered some login form with DES and other encryption to encrypt the parameters, that is, you do not take care of these front-end encryption, you write the script is not possible Login successfully. For this problem, there are now three ways to solve it:

  • ① Read and understand the front-end encryption process, and then script these methods (or find open source source code) to simulate this encryption process. The disadvantage is: if you don't know JS, the cost of reading it is higher;
  • ② selenium + Chrome Headless. disadvantage: because it is simulated click, so the efficiency is lower than ① and ③;
  • ③ Use the language to call the JS engine to execute the JS function. The disadvantage is: the effect of each JS engine execution will be inconsistent, resulting in some small deviations;
  •   In the third way, Python language can utilize the library PyExecJS, PyV8, Js2Py three, PyV8 use on the error (I have not solved for the time being), Js2Py is equivalent to the JS translated into Pyhton, Js2Py for complex JS is very easy to error. So this article mainly discusses the process of executing JS functions through the Python language's PyExecJS library (switching between different JS engines).

PyExecJS official website case

pip installs.

pip install PyExecJS

Demo:

import execjs

print(("'red yellow blue'.split(' ')"))

ctx = ("""
  function add(x, y) {
   return x + y;
  }
 """)
print(("add", 1, 2))

Output.

['red', 'yellow', 'blue'] 3

View JS engine information

# 1. on windows do not need other dependencies to run execjs, because there is a default JScript library, if you want to run other JS engine library, you need to install a separate.
 # windows default execution of the JS environment
  ().name
  # Return Value: JScript
 # If you want to switch, use ["EXECJS_RUNTIME"] = "XXX", you must configure environment variables if you just installed another JS engine, and you may need to restart your computer or restart your IDE.
 # If you have windows, you can switch to Node #
  ["EXECJS_RUNTIME"] = "Node"
  print(().name)
  # Return value: (V8)
 # If you have PhantomJSs on Windows, you can switch to PhantomJS.
  ["EXECJS_RUNTIME"] = "PhantomJS"
  print(().name)
  # Return Value: PhantomJS
 
# 2. In ubuntu need to install the implementation of JS environment dependencies, the author's environment for PhantomJS
  ().name
  # Return Value: PhantomJS
 
# 3. given in the source code, executable execjs environment:
 PyV8   = "PyV8"
 Node   = "Node"
 JavaScriptCore = "JavaScriptCore"
 SpiderMonkey = "SpiderMonkey"
 JScript  = "JScript"
 PhantomJS  = "PhantomJS"
 SlimerJS  = "SlimerJS"
 Nashorn  = "Nashorn"

Steps to install PhantomJS

Download Address:

 / 

Copy the script into your Python environment:

Unzip the downloaded file and find the directory.\phantomjs-2.1.1\bin\under the python folder you're using, move it to Script under the python folder you're using.

# Examples Anaconda3
D:\programfiles\Anaconda3\Scripts

Add system variables:


D:\programfiles\Anaconda3\Add to system variables.


Validation:

After adding the environment variable, verify in cmd that you can use the phantomjs command, indicating that the environment is set up.


Switch to PhantomJS in python:

["EXECJS_RUNTIME"] = "PhantomJS"

Case 1

1. Visit the login page of the target website and view the source code

  Visit / to see what js does with the input account, password before submitting the form. (The following is pseudo code)

<html>
<head></head>
<script src="/js/"></script>
<script>
 function password(psw, code, acc) {
  return "[p]" + (CryptoJS.MD5(CryptoJS.MD5(CryptoJS.MD5(psw).toString() + code).toString()).toString() + "@" + acc, code);
 }
 
 function doencodeacc(acc, code) {
  alert("[p]" + (acc, code));
 }
 doencodeacc("zhansan123456","pYr6BTle");
</script>
<body>

</body>
 
</html>

2. Put the js in the same directory as the py script.

  I've pasted the entire file here for those who need to experiment.

/*
CryptoJS v3.1.2
/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(u,p){var d={},l=={},s=function(){},t=={extend:function(a){=this;var c=new s;a&&(a);("init")||(=function(){c.$(this,arguments)});=c;c.$super=this;return c},create:function(){var a=();(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)(c)&&(this[c]=a[c]);("toString")&&(=)},clone:function(){return (this)}},
r==({init:function(a,c){a==a||[];=c!=p?c:4*},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=,e=,j=;a=;();if(j%4)for(var k=0;k<a;k++)c[j+k>>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535<)for(k=0;k<a;k+=4)c[j+k>>>2]=e[k>>>2];else (c,e);+=a;return this},clamp:function(){var a=,c=;a[c>>>2]&=4294967295<<
32-8*(c%4);=(c/4)},clone:function(){var a=(this);=(0);return a},random:function(a){for(var c=[],e=0;e<a;e+=4)(4294967296*()|0);return new (c,a)}}),w=={},v=={stringify:function(a){var c=;a=;for(var e=[],j=0;j<a;j++){var k=c[j>>>2]>>>24-8*(j%4)&255;((k>>>4).toString(16));((k&15).toString(16))}return ("")},parse:function(a){for(var c=,e=[],j=0;j<c;j+=2)e[j>>>3]|=parseInt((j,
2),16)<<24-4*(j%8);return new (e,c/2)}},b=w.Latin1={stringify:function(a){var c=;a=;for(var e=[],j=0;j<a;j++)((c[j>>>2]>>>24-8*(j%4)&255));return ("")},parse:function(a){for(var c=,e=[],j=0;j<c;j++)e[j>>>2]|=((j)&255)<<24-8*(j%4);return new (e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape((a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return (unescape(encodeURIComponent(a)))}},
q==({reset:function(){this._data=new ;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=(a));this._data.concat(a);this._nDataBytes+=},_process:function(a){var c=this._data,e=,j=,k=,b=j/(4*k),b=a?(b):((b|0)-this._minBufferSize,0);a=b*k;j=(4*a,j);if(a){for(var q=0;q<a;q+=k)this._doProcessBlock(e,q);q=(0,a);-=j}return new (q,j)},clone:function(){var a=(this);
a._data=this._data.clone();return a},_minBufferSize:0});=({cfg:(),init:function(a){=(a);()},reset:function(){(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,e){return(new (e)).finalize(b)}},_createHmacHelper:function(a){return function(b,e){return(new (a,
e)).finalize(b)}}});var n=={};return d}(Math);
(function(){var u=CryptoJS,p=;.Base64={stringify:function(d){var l=,p=,t=this._map;();d=[];for(var r=0;r<p;r+=3)for(var w=(l[r>>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v<p;v++)((w>>>6*(3-v)&63));if(l=(64))for(;%4;)(l);return ("")},parse:function(d){var l=,s=this._map,t=(64);t&&(t=(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w<
l;w++)if(w%4){var v=((w-1))<<2*(w%4),b=((w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return (t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<<j|b>>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<<j|b>>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<<j|b>>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<<j|b>>>32-j)+n}for(var t=CryptoJS,r=,w=,v=,r=,b=[],x=0;64>x;x++)b[x]=4294967296*((x+1))|0;r=r.MD5=({_doReset:function(){this._hash=new ([1732584193,4023233417,2562383102,271733878])},
_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]),
f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f,
m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m,
E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=,a=8*this._nDataBytes,c=8*;n[c>>>5]|=128<<24-c%32;var e=(a/
4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;=4*(+1);this._process();b=this._hash;n=;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math);
(function(){var u=CryptoJS,p=,d=,l=,p=,s==({cfg:({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){=(d)},compute:function(d,r){for(var p=,s=(),b=(),u=,q=,p=;<q;){n&&(n);var n=(d).finalize(r);();for(var a=1;a<p;a++)n=(n),();(n)}=4*q;return b}});=function(d,l,p){return (p).compute(d,
l)}})();
||function(u){var p=CryptoJS,d=,l=,s=,t=,r=.Base64,w=,v==({cfg:(),createEncryptor:function(e,a){return (this._ENC_XFORM_MODE,e,a)},createDecryptor:function(e,a){return (this._DEC_XFORM_MODE,e,a)},init:function(e,a,b){=(b);this._xformMode=e;this._key=a;()},reset:function(){(this);this._doReset()},process:function(e){this._append(e);return this._process()},
finalize:function(e){e&&this._append(e);return this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(e){return{encrypt:function(b,k,d){return("string"==typeof k?c:a).encrypt(e,b,k,d)},decrypt:function(b,k,d){return("string"==typeof k?c:a).decrypt(e,b,k,d)}}}});=({_doFinalize:function(){return this._process(!0)},blockSize:1});var b=={},x=function(e,a,b){var c=this._iv;c?this._iv=u:c=this._prevBlock;for(var d=0;d<b;d++)e[a+d]^=
c[d]},q=(=({createEncryptor:function(e,a){return (e,a)},createDecryptor:function(e,a){return (e,a)},init:function(e,a){this._cipher=e;this._iv=a}})).extend();=({processBlock:function(e,a){var b=this._cipher,c=;(this,e,a,c);(e,a);this._prevBlock=(a,a+c)}});=({processBlock:function(e,a){var b=this._cipher,c=,d=(a,a+c);(e,a);(this,
e,a,c);this._prevBlock=d}});b==q;q=(={}).Pkcs7={pad:function(a,b){for(var c=4*b,c=%c,d=c<<24|c<<16|c<<8|c,l=[],n=0;n<c;n+=4)(d);c=(l,c);(c)},unpad:function(a){-=[-1>>>2]&255}};=({cfg:({mode:b,padding:q}),reset:function(){(this);var a=,b=,a=;if(this._xformMode==this._ENC_XFORM_MODE)var c=;else c=,this._minBufferSize=1;this._mode=(a,
this,b&&)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=;if(this._xformMode==this._ENC_XFORM_MODE){(this._data,);var b=this._process(!0)}else b=this._process(!0),(b);return b},blockSize:4});var n==({init:function(a){(a)},toString:function(a){return(a||).stringify(this)}}),b=(={}).OpenSSL={stringify:function(a){var b=;a=;return(a?([1398893684,
1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=(a);var b=;if(1398893684==b[0]&&1701076831==b[1]){var c=((2,4));(0,4);-=16}return ({ciphertext:a,salt:c})}},a==({cfg:({format:b}),encrypt:function(a,b,c,d){d=(d);var l=(c,d);b=(b);l=;return ({ciphertext:b,key:c,iv:,algorithm:a,mode:,padding:,blockSize:,formatter:})},
decrypt:function(a,b,c,d){d=(d);b=this._parse(b,);return (c,d).finalize()},_parse:function(a,b){return"string"==typeof a?(a,this):a}}),p=(={}).OpenSSL={execute:function(a,b,c,d){d||(d=(8));a=({keySize:b+c}).compute(a,d);c=((b),4*c);=4*b;return ({key:a,iv:c,salt:d})}},c==({cfg:({kdf:p}),encrypt:function(b,c,d,l){l=(l);d=(d,
,);=;b=(this,b,c,,l);(d);return b},decrypt:function(b,c,d,l){l=(l);c=this._parse(c,);d=(d,,,);=;return (this,b,c,,l)}})}();
(function(){for(var u=CryptoJS,p=,d=,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8,
16,32,64,128,27,54],d==({_doReset:function(){for(var a=this._key,c=,d=/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j<a;j++)if(j<d)e[j]=c[j];else{var k=e[j-1];j%d?6<d&&4==j%d&&(k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;d<a;d++)j=a-d,k=d%4?e[j]:e[j-4],c[d]=4>d||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>>
8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r<m;r++)var q=d[g>>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t=
d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});=p._createHelper(d)})();function _k(h){var e=h+"0123456789abcdef";return (0, 16);}
 = function (d,p) {
 var key = .(_k(p));
 var encrypted = (d, key, {
  iv: key,
  mode: ,
  padding: .Pkcs7
 });
 return ();
};

function doencodepsw(psw, code, acc) {
   return "[p]" + (CryptoJS.MD5(CryptoJS.MD5(CryptoJS.MD5(psw).toString() + code).toString()).toString() + "@" + acc, code);
}

function doencodeacc(acc, code) {
 return "[p]" + (acc, code);
}

3. Write Python scripts to call js

import execjs

def get_js():
 # Open the JS file
 f = open("", 'r', encoding='utf-8')
 line = ()
 htmlstr = ''
 while line:
  htmlstr = htmlstr + line
  line = ()
 return htmlstr

def get_des_psswd(acc, code):
 jsstr = get_js()
 # Load JS files
 ctx = (jsstr)
 # call js method The first parameter is the JS method name, followed by the parameters of the js method
 return ('doencodeacc', acc, code)

if __name__ == '__main__': 
 print(get_des_psswd("zhangsan123456", "pYr6BTle")) # pYr6BTle = ralt code (Encrypted salt value)

Case 2

  This case will be relatively more complicated;

1. Find the encryption function at login

look at outsourcing




  When I logged into the site, I found that the site first asynchronously obtain a public key, and then the account and password are encrypted, only on the debugging. It is recommended to use Chrome F12 for debugging, other tools are not as good as him.



Right clicking on the source code reveals that there is JS, but too much to filter.

2. Listen to the mouse click event to see the process


After one operation, jump to the login submit function, directly look at the code


After looking down at the code, found that the code did not add obfuscation and there is no other protection, then a one-step solution, crack the encryption is not as simple as cutting vegetables.


3. Write code

Import js.

It will be imported into a python project, this js is over 5k lines, so I won't paste it up.


import execjs
import os

if __name__ == '__main__':
 # After switching between JScript and Node, I can't execute JS, and found that PhantomJS is still reliable!
 # ["EXECJS_RUNTIME"] = "JScript"
 # ["EXECJS_RUNTIME"] = "Node"
 ["EXECJS_RUNTIME"] = "PhantomJS"
 print(().name)

 js = open('', encoding='utf-8').read()
 jo = (js)
 pwd = ('myf')
 print(pwd)

Output:

PhantomJS
Hu4Ujwqwe/PDAblIvjNyrX4NrltoRXXDdyC6+F+p0LaqPSegMZ16oIqeVPiHlh5x8zKeI2DC3DoPVf8ZlusUCQ==

Different browser kernel versions handle URL encoding differently [cold knowledge]

  When an Html form is submitted, each form parameter is Url-encoded before it is sent. For historical reasons, the Url encoding implementations used by forms do not conform to the latest standards. For example, the encoding used for spaces is not%20Rather+If the form is submitted using the Post method, we can see a Content-Type header in the HTTP header with the value application/x-www-form-urlencoded. Most applications can handle this non-standard implementation of Url encoding, but on the client-side Javascript, there is no function that can decode the + sign into a space, you have to write your own conversion function. Also, for non-ASCII characters, the character set used depends on the character set of the current document.

summarize

The above is a small introduction to the case study of Python using the PyExecJS library to execute JS functions, I hope to help you, if you have any questions please leave me a message, I will reply to you in a timely manner. I would also like to thank you very much for your support of my website!
If you find this article helpful, please feel free to reprint it, and please note the source, thank you!