getgrav / grav-theme-soraarticle

Sora Article is a minimal Theme designed for blogger, ported to Grav.
https://getgrav.org
Other
5 stars 4 forks source link

Malware Signature Triggered #12

Open mricha41 opened 4 years ago

mricha41 commented 4 years ago

The file "https://github.com/getgrav/grav-theme-soraarticle/blob/develop/js/soraarticle.js" caused a malware trigger at my host. I did not use the theme but installed it recently to try out, that's when I received a notification and had to remove the theme. If you view the file, it is quite unusual in comparison to other Grav theme .js files (for example, antimatter.js in the "Antimatter" theme is not a giant wall of hex values). This is probably worth someone's attention at the very least.

leycec commented 4 years ago

That's... actually pretty insane. Intentional obfuscation in production JavaScript is a significant concern – both from the obvious security perspective and the non-obvious cost in time complexity required for client browsers to run the absurd RegExp()-based while() loop embedded within the miserable bowels of that script.

Does anyone have any insight as to what, exactly, is going on? Is the upstream SoraArticle codebase this decrepit as well? Can this be reasonably deobfuscated?

Relatedly, all of the other scripts bundled with this theme come pre-minified. That's bad. Grav itself already optionally performs cached minification, so there's no benefit whatsoever for this theme to force that option on developers. Pre-minification is additional obfuscation, which only compounds matters here.

As is, the JavaScript portion of this theme is neither maintainable nor auditable – which is a blatant shame. This is one of the more aesthetically pleasing and well-maintained open-source Grav themes, but it's hard to recommend as is. A significant rethink is warranted. </rubs_aching_forehead>

leycec commented 4 years ago

So, upstream is insane. Because Capitalism™, there's no published repository for the original SoraArticle blogger template – only a zipfile polluted with useless advertising and a single useful file named "Sora Article Free Version.xml". That file inlines everything – HTML, CSS, JavaScript, and Blogger-specific conditional logic in the form of XML comments.

Like, whatevah. That's not the issue. The issue is that the JavaScript embedded in that file no longer appears to conform to the JavaScript bundled with this theme. Upstream appears to have obfuscated what we refer to as the soraarticle.js script using a different algorithm. Here's what that file effectively looks like under the most recent upstream release:

<script type='text/javascript'>
//<![CDATA[
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('X 1e(D){3 8=G;3 d=W V();3 7=1;3 9=1;3 r=0;3 z=0;3 x=0;3 5=\'\';3 m=\'\';3 y=\'\';A(3 i=0,g;g=D.16.Z[i];i++){3 P=g.I.$t.C(0,19)+g.I.$t.C(13,12);B=10(P);3 n=g.n.$t;4(n!=\'\'){4(r==0||(r%u==(u-1))){4(8.c(B)!=-1){7=9}4(n!=\'\')9++;d[d.j]=\'/l?N-f=\'+B+\'&f-F=\'+u}}r++}A(3 p=0;p<d.j;p++){4(p>=(7-M-1)&&p<(7+M)){4(z==0&&p==7-2){4(7==2){m=\'<6 b="H"><a h="/">\'+J+\'</a></6>\'}k{m=\'<6 b="H"><a h="\'+d[p]+\'">\'+J+\'</a></6>\'}z++}4(p==(7-1)){5+=\'<6 b="15">\'+7+\'</6>\'}k{4(p==0){5+=\'<6 b="L"><a h="/">1</a></6>\'}k{5+=\'<6 b="L"><a h="\'+d[p]+\'">\'+(p+1)+\'</a></6>\'}}4(x==0&&p==7){y=\'<6 b="H"> <a h="\'+d[p]+\'">\'+Y+\'</a></6>\';x++}}}4(7>1){5=\'\'+m+\' \'+5+\' \'}5=\'<K b="U"><6 11="1b: #1p;" b="1n"> 1m (\'+(9-1)+\')</6>\'+5;4(7<(9-1)){5+=y}4(9==1)9++;5+=\'</K>\';3 e=E.18("e");3 w=E.1k("1q-1o");4(9<=2){5=\'\'}A(3 p=0;p<e.j;p++){e[p].O=5}4(e&&e.j>0){5=\'\'}4(w){w.O=5}}X 1l(D){3 8=G;3 d=W V();3 S=8.c("/l/o/")!=-1;3 s=S?8.17(8.c("/l/o/")+14,8.j):"";s=s.c("?")!=-1?s.17(0,s.c("?")):s;3 7=1;3 9=1;3 r=0;3 z=0;3 x=0;3 5=\'\';3 m=\'\';3 y=\'\';3 Q=\'<6 b="L"><a h="/l/o/\'+s+\'?&f-F=\'+u+\'">\';3 8=G;A(3 i=0,g;g=D.16.Z[i];i++){3 P=g.I.$t.C(0,19)+g.I.$t.C(13,12);B=10(P);3 n=g.n.$t;4(n!=\'\'){4(r==0||(r%u==(u-1))){4(8.c(B)!=-1){7=9}4(n!=\'\')9++;d[d.j]=\'/l/o/\'+s+\'?N-f=\'+B+\'&f-F=\'+u}}r++}A(3 p=0;p<d.j;p++){4(p>=(7-M-1)&&p<(7+M)){4(z==0&&p==7-2){4(7==2){m=Q+J+\'</a></6>\'}k{m=\'<6 b="H"><a h="\'+d[p]+\'">\'+J+\'</a></6>\'}z++}4(p==(7-1)){5+=\'<6 b="15">\'+7+\'</6>\'}k{4(p==0){5=Q+\'1</a></6>\'}k{5+=\'<6 b="L"><a h="\'+d[p]+\'">\'+(p+1)+\'</a></6>\'}}4(x==0&&p==7){y=\'<6 b="H"> <a h="\'+d[p]+\'">\'+Y+\'</a></6>\';x++}}}4(7>1){4(!S){5=\'\'+m+\' \'+5+\' \'}k{5=\'\'+m+\' \'+5+\' \'}}5=\'<K b="U"><6 11="1b: #1p;" b="1n"> 1m (\'+(9-1)+\')</6>\'+5;4(7<(9-1)){5+=y}4(9==1)9++;5+=\'</K>\';3 e=E.18("e");3 w=E.1k("1q-1o");4(9<=2){5=\'\'}A(3 p=0;p<e.j;p++){e[p].O=5}4(e&&e.j>0){5=\'\'}4(w){w.O=5}}3 G=1v.h;3 8=G;4(8.c("/l/o/")!=-1){4(8.c("?N-f")!=-1){3 T=8.C(8.c("/l/o/")+14,8.c("?N-f"))}k{3 T=8.C(8.c("/l/o/")+14,8.c("?&f"))}}3 R="/";4(8.c("?q=")==-1){4(8.c("/l/o/")==-1){E.1h(\'<v 1g="\'+R+\'1f/1d/1r?1c=D-1j-v&1i=1e&f-F=1a" ><\\/v>\')}k{E.1h(\'<v 1g="\'+R+\'1f/1d/1t/-/\'+T+\'?1c=D-1j-v&1i=1l&f-F=1a" ><\\/v>\')}$(\'#1u\').1x(\'1s\',\'1w\')}',62,96,'|||var|if|html|span|thisNum|thisUrl|postNum||class|indexOf|htmlMap|pageArea|max|post|href||length|else|search|upPageHtml|title|label|||itemCount|thisLable||pageCount|script|blogPager|eFlag|downPageHtml|fFlag|for|timestamp|substring|json|document|results|home_page_url|showpage|published|upPageWord|div|showpageNum|displayPageNum|updated|innerHTML|timestamp1|labelHtml|home_page|isLablePage|lblname1|showpageArea|Array|new|function|downPageWord|entry|encodeURIComponent|style|29|23||showpagePoint|feed|substr|getElementsByName||99999|COLOR|alt|posts|showpageCount|feeds|src|write|callback|in|getElementById|showpageCount2|Pages|showpageOf|pager|000|blog|summary|visibility|full|mycontent|location|visible|css'.split('|'),0,{}))
//]]>
</script>

Completely different, right? That's even worse than the current script bundled with this theme! In optimistic theory, we might be able to reverse engineer either that script or the above snippet by leveraging any of several well-known JavaScript deobfuscators. My intuitive suspicion is that the current script bundled with this theme is more-or-less trivially deobfuscatable – which probably explains why they replaced it with something even more horribly obfuscated.

I am Jack's extreme distaste for corporate antics.

leycec commented 4 years ago

Bwa-haha! Suck it, feckless corporate cubicle drones. Open-source volunteerism easily triumphs over villainous security-through-obscurity... yet again.

I can't particularly take credit for this, but the current script bundled with this theme is indeed trivially deobfuscatable. That explains why upstream substituted this script with a "new" script that's probably just re-obfuscated but otherwise equivalent to the original. In any case, piping the contents of the original through DeobfuscateJavaScript yields human-readable sanity:

function stripTags(s, n) {
    return s.replace(/<.*?>/ig, '').split(/\s+/).slice(0, n - 1).join(' ')
}
function readmore(a) {
    var b = 42;
    var p = document.getElementById(a);
    imgtag = "";
    ifrtag = "";
    img = p.getElementsByTagName("img");
    ifr = p.getElementsByTagName("iframe");
    if (ifr.length >= 1) ifrtag = '<div class="blog-content-wrapper" ><div class="gdlr-blog-thumbnail gdlr-video"><iframe width="750px" height="330px" src="' + ifr[0].src + '" frameborder="0" allowfullscreen style="display:block;"></iframe></div>';
    else if (img.length >= 1) imgtag = '<div class="blog-content-wrapper" ><div class="gdlr-blog-thumbnail"><a href="' + post_url + '"> <img src="' + img[0].src + '" width="750px" height="330px" /></a><div class="gdlr-sticky-banner">' + time + '</div></div>';
    else imgtag = '<div class="blog-content-wrapper" >';
    p.innerHTML = ifrtag + imgtag + '<div class="blog-content-inner-wrapper"><header class="post-header"><h3 class="gdlr-blog-title"><a href="' + post_url + '">' + post_title + '</a></h3><div class="gdlr-blog-info gdlr-title-font gdlr-info"><div class="blog-info blog-tag"><i class="icon-tags"></i> ' + label + '</div><div class="blog-info blog-author"><i class="icon-user"></i><a href="' + author_url + '" title="Posts by ' + author_name + '" rel="author">' + author_name + '</a></div><span class="gdlr-seperator">/</span><div class="blog-info blog-comments"><i class="icon-comments-alt"></i><a href="' + post_url + '" >' + comment + ' Comments</a></div><div class="clear"></div></div></header><!-- entry-header --><div class="gdlr-blog-content"><p>' + stripTags(p.innerHTML, b) + '</p><p> <a href="' + post_url + '" class="more-link"><span class="gdlr-button with-border excerpt-read-more">Read More</span></a></p><div class="gdlr-social-share"><a href="http://digg.com/submit?url=' + post_url + '&#038;title=' + post_title + '" target="_blank"><img src="http://themes.goodlayers2.com/simplearticle/wp-content/themes/simplearticle/images/dark/social-share/digg.png" alt="digg-share" width="112" height="112" /></a><a href="http://www.facebook.com/share.php?u=' + post_url + '" target="_blank"><img src="http://themes.goodlayers2.com/simplearticle/wp-content/themes/simplearticle/images/dark/social-share/facebook.png" alt="facebook-share" width="112" height="112" /></a><a href="http://www.linkedin.com/shareArticle?mini=true&#038;url=' + post_url + '&#038;title=' + post_title + '" target="_blank"><img src="http://themes.goodlayers2.com/simplearticle/wp-content/themes/simplearticle/images/dark/social-share/linkedin.png" alt="linked-share" width="112" height="112" /></a><a href="http://www.tumblr.com/share/link?url=' + post_url + '&amp;name=' + post_title + '" target="_blank"><img src="http://themes.goodlayers2.com/simplearticle/wp-content/themes/simplearticle/images/dark/social-share/tumblr.png" alt="tumblr-share" width="112" height="112" /></a><a href="http://reddit.com/submit?url=' + post_url + '&#038;title=' + post_title + '" target="_blank"><img src="http://themes.goodlayers2.com/simplearticle/wp-content/themes/simplearticle/images/dark/social-share/reddit.png" alt="reddit-share" width="112" height="112" /></a><a href="http://www.stumbleupon.com/submit?url=' + post_url + '&#038;title=' + post_title + '" target="_blank"><img src="http://themes.goodlayers2.com/simplearticle/wp-content/themes/simplearticle/images/dark/social-share/stumble-upon.png" alt="stumble-upon-share" width="112" height="112" /></a><a href="http://twitter.com/home?status=' + post_title + '-' + post_url + '" target="_blank"><img src="http://themes.goodlayers2.com/simplearticle/wp-content/themes/simplearticle/images/dark/social-share/twitter.png" alt="twitter-share" width="112" height="112" /></a><div class="clear"></div></div></div></div> </div>'
}
rn = "<h5>No related post available</h5>";
rcomment = "comments";
rdisable = "disable comments";
commentYN = "no";
var dw = "";
titles = new Array;
titlesNum = 0;
urls = new Array;
timeR = new Array;
thumb = new Array;
commentsNum = new Array;
comments = new Array;

function related_results_labels(c) {
    for (var b = 0; b < c.feed.entry.length; b++) {
        var d = c.feed.entry[b];
        titles[titlesNum] = d.title.$t;
        for (var a = 0; a < d.link.length; a++) {
            if ("thr$total" in d) commentsNum[titlesNum] = d.thr$total.$t + " " + rcomment;
            else commentsNum[titlesNum] = rdisable;
            if (d.link[a].rel == "alternate") {
                urls[titlesNum] = d.link[a].href;
                timeR[titlesNum] = d.published.$t;
                if ("media$thumbnail" in d) thumb[titlesNum] = d.media$thumbnail.url;
                else thumb[titlesNum] = "http://lh3.ggpht.com/--Z8SVBQZ4X8/TdDxPVMl_sI/AAAAAAAAAAA/jhAgjCpZtRQ/no-image.png";
                titlesNum++;
                break
            }
        }
    }
}
function removeRelatedDuplicates() {
    var b = new Array(0);
    c = new Array(0);
    e = new Array(0);
    f = new Array(0);
    g = new Array(0);
    for (var a = 0; a < urls.length; a++) if (!contains(b, urls[a])) {
        b.length += 1;
        b[b.length - 1] = urls[a];
        c.length += 1;
        c[c.length - 1] = titles[a];
        e.length += 1;
        e[e.length - 1] = timeR[a];
        f.length += 1;
        f[f.length - 1] = thumb[a];
        g.length += 1;
        g[g.length - 1] = commentsNum[a]
    }
    urls = b;
    titles = c;
    timeR = e;
    thumb = f;
    commentsNum = g
}
function contains(b, d) {
    for (var c = 0; c < b.length; c++) if (b[c] == d) return true;
    return false
}
function printRelatedLabels(a) {
    var y = a.indexOf("?m=0");
    if (y != -1) a = a.replace(/\?m=0/g, "");
    for (var b = 0; b < urls.length; b++) if (urls[b] == a) {
        urls.splice(b, 1);
        titles.splice(b, 1);
        timeR.splice(b, 1);
        thumb.splice(b, 1);
        commentsNum.splice(b, 1)
    }
    var c = Math.floor((titles.length - 1) * Math.random());
    var b = 0;
    if (titles.length == 0) dw += rn;
    else {
        while (b < titles.length && b < 20 && b < 4) {
            if (y != -1) urls[c] = urls[c] + "?m=0";
            if (commentYN == "yes") comments[c] = " - " + commentsNum[c];
            else comments[c] = "";
            dw += '<div class="related-post-widget six columns"><div class="related-post-widget-thumbnail"><a href="' + urls[c] + '" ><img src="' + thumb[c].replace(/\/s72\-c/, "/s" + 250 + "") + '" alt="" width="150" height="150" /></a></div><div class="related-post-widget-content"><div class="related-post-widget-title"><a href="' + urls[c] + '" >' + titles[c] + '</a></div></div><div class="clear"></div></div>';
            if (c < titles.length - 1) c++;
            else c = 0;
            b++
        }
    }
    urls.splice(0, urls.length);
    titles.splice(0, titles.length);
    document.getElementById("related-posts").innerHTML = dw
};
$(document).ready(function() {
    $('#mycontent').html("Designed By <a href='http://www.soratemplates.com/' rel='dofollow' target='_blank' title='Blogger Templates'>Sora Templates</a> and <a href='http://mybloggerthemes.com/' rel='dofollow' target='_blank' title='Free Blogger Templates'>My Blogger Themes</a>");
    setInterval(function() {
        if (!$('#mycontent:visible').length) window.location.href = 'http://www.soratemplates.com/'
    }, 3000)
});

A superficial reading of the above shows no malicious payloads. Of course, there are various forced advertisements for upstream... but I'm entirely fine with that. I can only assume they obfuscated in the first place to prevent removal of said advertisements – which is frankly absurd and shows a complete disregard for best practices. All they had to do was license the free variants of their templates under the GPL and then offer commercial non-GPL licensing for clients requiring custom modifications. So, basically the PyQt development model gone webdev.

Instead, they intentionally destroyed their own codebase.

leycec commented 4 years ago

...wat we do now?

Let me tell you what now. I'm a bit preoccupied with raging webdev fires elsewhere. Instead, it'd be swell if someone who is not me submitted a pull request replacing the contents of the existing obfuscated js/soraarticle.js file with the deobfuscated text above.

Additionally, it'd be swell if someone who is not me submitted another pull request replacing the contents of the other pre-minified files in the js/ subdirectory with non-minified variants. Consider either:

Thanks a bunch, all! We'll sanitize this unwarranted misery one way... or another.

pushbuttonreceivecode commented 4 years ago

First off - thanks for looking into this! I, and surely others, owe you gratitude. This sounds like a lot of work for maybe little benefit. I'd look at the download/installed stats if you have them and delete the repo (and Grav theme site reference to it) if nobody is really using it.

leycec commented 4 years ago

First off - thanks for looking into this! I, and surely others, owe you gratitude.

Oh, you. :relaxed:

This sounds like a lot of work for maybe little benefit.

It does... and it doesn't. Replacing the existing obfuscated js/soraarticle.js file with the deobfuscated text above is trivial. Given the security implications of obfuscated JavaScript, it's a bit concerning that nobody – this means you, repository maintainers! – has bothered to do so yet. That tells me this repository is on life support, which is both helpful to know and concerning for anyone intending to use this theme in production. (Hint: don't.)

Replacing the existing minified JavaScript and CSS files with non-minified variants is definitely a taller order. The benefits are mostly developer-side – which probably explains why nobody's done it. Still, this theme's existing codebase is basically infeasible to maintain as is. Between the extreme minification and obfuscation, complete lack of internal comments or documentation, and ominous silence from repository maintainers, it's easy to see this theme simply dying a quiet death over the next several years. So it goes.

For example, especially problematic files include:

Since The Powers That Be no longer appear to care about this theme, I'm contemplating a well-maintained friendly fork by (waitforit) me. That's not something I'd blindly leap into without just cause, but... well, here we are. Just cause is this whole thread.

I'd look at the download/installed stats if you have them and delete the repo (and Grav theme site reference to it) if nobody is really using it.

I'm... not sure I understand that sentence. If you're suggesting the entire repository be summarily abandoned because of a single non-malicious JavaScript file, I'd disagree with that assessment. The sky isn't falling yet. It just needs a delicate massage from someone with sufficient volunteer time who actually cares – which might mean me, actually.

Moreover, I'm not a repository maintainer. I don't have push access to this repository. Even if I wanted to archive it (hot take: I don't), I couldn't. What I do increasingly want to do is fork this repository and reignite the development effort under my "personal leadership." ...cue vomitting into own mouth Between this issue, Grav 2.0 compliance, internationalization, and the glut of unresolved issues, there's certainly more than enough for everyone to do.

Stay tuned to this comment thread for more titillating excitement, folks. :tv: